ormbridge 0.0.0-stage → 0.2.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +20 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cliRunner.d.ts +20 -0
  7. package/dist/cliRunner.js +343 -0
  8. package/dist/cliRunner.js.map +1 -0
  9. package/dist/config.d.ts +40 -0
  10. package/dist/config.js +204 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/convert.d.ts +20 -0
  13. package/dist/convert.js +61 -0
  14. package/dist/convert.js.map +1 -0
  15. package/dist/emitters/django.d.ts +4 -0
  16. package/dist/emitters/django.js +501 -0
  17. package/dist/emitters/django.js.map +1 -0
  18. package/dist/emitters/doctrine.d.ts +20 -0
  19. package/dist/emitters/doctrine.js +1134 -0
  20. package/dist/emitters/doctrine.js.map +1 -0
  21. package/dist/emitters/graphene.d.ts +4 -0
  22. package/dist/emitters/graphene.js +431 -0
  23. package/dist/emitters/graphene.js.map +1 -0
  24. package/dist/emitters/laravel.d.ts +33 -0
  25. package/dist/emitters/laravel.js +1695 -0
  26. package/dist/emitters/laravel.js.map +1 -0
  27. package/dist/emitters/prisma.d.ts +21 -0
  28. package/dist/emitters/prisma.js +1058 -0
  29. package/dist/emitters/prisma.js.map +1 -0
  30. package/dist/emitters/typeorm.d.ts +9 -0
  31. package/dist/emitters/typeorm.js +833 -0
  32. package/dist/emitters/typeorm.js.map +1 -0
  33. package/dist/emitters/typescriptInterfaces.d.ts +15 -0
  34. package/dist/emitters/typescriptInterfaces.js +350 -0
  35. package/dist/emitters/typescriptInterfaces.js.map +1 -0
  36. package/dist/formats.d.ts +80 -0
  37. package/dist/formats.js +212 -0
  38. package/dist/formats.js.map +1 -0
  39. package/dist/index.d.ts +33 -0
  40. package/dist/index.js +18 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/io.d.ts +48 -0
  43. package/dist/io.js +328 -0
  44. package/dist/io.js.map +1 -0
  45. package/dist/ir.d.ts +197 -0
  46. package/dist/ir.js +10 -0
  47. package/dist/ir.js.map +1 -0
  48. package/dist/naming.d.ts +11 -0
  49. package/dist/naming.js +88 -0
  50. package/dist/naming.js.map +1 -0
  51. package/dist/parsers/django.d.ts +14 -0
  52. package/dist/parsers/django.js +1161 -0
  53. package/dist/parsers/django.js.map +1 -0
  54. package/dist/parsers/doctrine.d.ts +12 -0
  55. package/dist/parsers/doctrine.js +1782 -0
  56. package/dist/parsers/doctrine.js.map +1 -0
  57. package/dist/parsers/laravel.d.ts +17 -0
  58. package/dist/parsers/laravel.js +2824 -0
  59. package/dist/parsers/laravel.js.map +1 -0
  60. package/dist/parsers/phpSyntax.d.ts +124 -0
  61. package/dist/parsers/phpSyntax.js +429 -0
  62. package/dist/parsers/phpSyntax.js.map +1 -0
  63. package/dist/parsers/prisma.d.ts +12 -0
  64. package/dist/parsers/prisma.js +964 -0
  65. package/dist/parsers/prisma.js.map +1 -0
  66. package/dist/parsers/pythonSyntax.d.ts +38 -0
  67. package/dist/parsers/pythonSyntax.js +120 -0
  68. package/dist/parsers/pythonSyntax.js.map +1 -0
  69. package/dist/parsers/typeorm.d.ts +12 -0
  70. package/dist/parsers/typeorm.js +1614 -0
  71. package/dist/parsers/typeorm.js.map +1 -0
  72. package/dist/parsers/typescriptSyntax.d.ts +57 -0
  73. package/dist/parsers/typescriptSyntax.js +200 -0
  74. package/dist/parsers/typescriptSyntax.js.map +1 -0
  75. package/dist/prismaOnlyConstructs.d.ts +7 -0
  76. package/dist/prismaOnlyConstructs.js +76 -0
  77. package/dist/prismaOnlyConstructs.js.map +1 -0
  78. package/dist/result.d.ts +20 -0
  79. package/dist/result.js +18 -0
  80. package/dist/result.js.map +1 -0
  81. package/dist/transforms.d.ts +13 -0
  82. package/dist/transforms.js +142 -0
  83. package/dist/transforms.js.map +1 -0
  84. package/package.json +83 -4
@@ -0,0 +1,2824 @@
1
+ import { basename } from 'node:path';
2
+ import { singularize, toPascalCase, toSnakeCase } from '../naming.js';
3
+ import { err, ok } from '../result.js';
4
+ import { argumentNodes, collectDeclarations, evaluateNode, getPhpParser, lastNameSegment, readEnumDeclaration, resolveClassName, } from './phpSyntax.js';
5
+ // ---------------------------------------------------------------------------
6
+ // Static tables
7
+ // ---------------------------------------------------------------------------
8
+ /** Framework classes an Eloquent model may extend, as fully qualified names. */
9
+ const MODEL_BASE_CLASSES = new Set([
10
+ 'Illuminate\\Database\\Eloquent\\Model',
11
+ 'Illuminate\\Foundation\\Auth\\User',
12
+ 'Illuminate\\Database\\Eloquent\\Relations\\Pivot',
13
+ 'Illuminate\\Database\\Eloquent\\Relations\\MorphPivot',
14
+ ]);
15
+ /** Short base class names accepted when the import cannot be resolved (for example a file read on its own). */
16
+ const MODEL_BASE_SHORT_NAMES = new Set([
17
+ 'Model',
18
+ 'Authenticatable',
19
+ 'Pivot',
20
+ 'MorphPivot',
21
+ ]);
22
+ const RELATION_METHODS = new Set([
23
+ 'hasOne',
24
+ 'hasMany',
25
+ 'belongsTo',
26
+ 'belongsToMany',
27
+ 'hasOneThrough',
28
+ 'hasManyThrough',
29
+ 'morphTo',
30
+ 'morphOne',
31
+ 'morphMany',
32
+ 'morphToMany',
33
+ 'morphedByMany',
34
+ 'hasOneOfMany',
35
+ 'latestOfMany',
36
+ 'oldestOfMany',
37
+ ]);
38
+ const RELATION_RETURN_TYPES = new Set([
39
+ 'HasOne',
40
+ 'HasMany',
41
+ 'BelongsTo',
42
+ 'BelongsToMany',
43
+ 'HasOneThrough',
44
+ 'HasManyThrough',
45
+ 'MorphTo',
46
+ 'MorphOne',
47
+ 'MorphMany',
48
+ 'MorphToMany',
49
+ 'Relation',
50
+ ]);
51
+ /** Methods that may follow a relationship call without changing which rows it relates. */
52
+ const HARMLESS_RELATION_MODIFIERS = new Set([
53
+ 'withPivot',
54
+ 'withTimestamps',
55
+ 'as',
56
+ 'using',
57
+ 'withDefault',
58
+ 'withoutGlobalScopes',
59
+ 'withoutGlobalScope',
60
+ 'orderBy',
61
+ 'orderByDesc',
62
+ 'latest',
63
+ 'oldest',
64
+ 'withCount',
65
+ 'with',
66
+ 'withTrashed',
67
+ 'onlyTrashed',
68
+ 'select',
69
+ 'addSelect',
70
+ 'distinct',
71
+ 'limit',
72
+ 'take',
73
+ 'inRandomOrder',
74
+ 'cascadeOnDelete',
75
+ ]);
76
+ const ON_DELETE_ACTIONS = {
77
+ cascade: 'cascade',
78
+ 'set null': 'setNull',
79
+ restrict: 'restrict',
80
+ 'no action': 'noAction',
81
+ 'set default': 'setDefault',
82
+ };
83
+ const GEOMETRY_TYPES = new Set([
84
+ 'geometry',
85
+ 'geography',
86
+ 'point',
87
+ 'lineString',
88
+ 'polygon',
89
+ 'geometryCollection',
90
+ 'multiPoint',
91
+ 'multiLineString',
92
+ 'multiPolygon',
93
+ 'multiPolygonZ',
94
+ ]);
95
+ /** `DB::` helpers that run SQL the parser cannot interpret. */
96
+ const RAW_SQL_METHODS = new Set([
97
+ 'statement',
98
+ 'unprepared',
99
+ ]);
100
+ /** Blueprint methods that change nothing the IR represents. */
101
+ const IGNORED_TABLE_METHODS = new Set([
102
+ 'engine',
103
+ 'charset',
104
+ 'collation',
105
+ 'comment',
106
+ 'temporary',
107
+ ]);
108
+ /** Schema methods that change nothing the IR represents. */
109
+ const IGNORED_SCHEMA_METHODS = new Set([
110
+ 'disableForeignKeyConstraints',
111
+ 'enableForeignKeyConstraints',
112
+ 'defaultStringLength',
113
+ 'defaultMorphKeyType',
114
+ 'morphUsingUuids',
115
+ 'morphUsingUlids',
116
+ ]);
117
+ /** Column modifiers that change nothing the IR represents. */
118
+ const IGNORED_COLUMN_MODIFIERS = new Set([
119
+ 'unsigned',
120
+ 'comment',
121
+ 'after',
122
+ 'first',
123
+ 'charset',
124
+ 'collation',
125
+ 'invisible',
126
+ 'algorithm',
127
+ 'language',
128
+ 'deferrable',
129
+ 'initiallyImmediate',
130
+ 'notValid',
131
+ 'lock',
132
+ ]);
133
+ const CONTROL_STATEMENTS = new Set([
134
+ 'if_statement',
135
+ 'foreach_statement',
136
+ 'for_statement',
137
+ 'while_statement',
138
+ 'do_statement',
139
+ 'switch_statement',
140
+ 'try_statement',
141
+ ]);
142
+ const CLAUSE_STATEMENTS = new Set([
143
+ 'else_clause',
144
+ 'else_if_clause',
145
+ 'catch_clause',
146
+ 'finally_clause',
147
+ 'case_statement',
148
+ 'default_statement',
149
+ 'switch_block',
150
+ 'colon_block',
151
+ ]);
152
+ const UNCOUNTABLE_NOUNS = new Set([
153
+ 'audio',
154
+ 'bison',
155
+ 'cattle',
156
+ 'chassis',
157
+ 'compensation',
158
+ 'data',
159
+ 'deer',
160
+ 'education',
161
+ 'emoji',
162
+ 'equipment',
163
+ 'evidence',
164
+ 'feedback',
165
+ 'firmware',
166
+ 'fish',
167
+ 'furniture',
168
+ 'gold',
169
+ 'hardware',
170
+ 'information',
171
+ 'knowledge',
172
+ 'media',
173
+ 'metadata',
174
+ 'money',
175
+ 'moose',
176
+ 'news',
177
+ 'nutrition',
178
+ 'offspring',
179
+ 'police',
180
+ 'rain',
181
+ 'rice',
182
+ 'series',
183
+ 'sheep',
184
+ 'software',
185
+ 'staff',
186
+ 'species',
187
+ 'swine',
188
+ 'traffic',
189
+ 'wheat',
190
+ ]);
191
+ const IRREGULAR_PLURALS = {
192
+ person: 'people',
193
+ man: 'men',
194
+ woman: 'women',
195
+ child: 'children',
196
+ foot: 'feet',
197
+ tooth: 'teeth',
198
+ goose: 'geese',
199
+ mouse: 'mice',
200
+ ox: 'oxen',
201
+ quiz: 'quizzes',
202
+ move: 'moves',
203
+ sex: 'sexes',
204
+ };
205
+ // ---------------------------------------------------------------------------
206
+ // Small helpers
207
+ // ---------------------------------------------------------------------------
208
+ function pushWarning(warnings, message) {
209
+ if (!warnings.includes(message)) {
210
+ warnings.push(message);
211
+ }
212
+ }
213
+ function unique(items) {
214
+ return [...new Set(items)];
215
+ }
216
+ function sameList(first, second) {
217
+ return (first.length === second.length &&
218
+ first.every((item, index) => item === second[index]));
219
+ }
220
+ function pluralizeWord(word) {
221
+ const lower = word.toLowerCase();
222
+ if (UNCOUNTABLE_NOUNS.has(lower)) {
223
+ return word;
224
+ }
225
+ const irregular = IRREGULAR_PLURALS[lower];
226
+ if (irregular !== undefined) {
227
+ return irregular;
228
+ }
229
+ const rules = [
230
+ [/(s)tatus$/i, '$1tatuses'],
231
+ [/(alias)$/i, '$1es'],
232
+ [/(matr|vert|ind)(ix|ex)$/i, '$1ices'],
233
+ [/(octop|vir)us$/i, '$1i'],
234
+ [/(ax|test)is$/i, '$1es'],
235
+ [/(x|ch|ss|sh)$/i, '$1es'],
236
+ [/([^aeiouy]|qu)y$/i, '$1ies'],
237
+ [/(?:([^f])fe|([lr])f)$/i, '$1$2ves'],
238
+ [/(lea)f$/i, '$1ves'],
239
+ [/sis$/i, 'ses'],
240
+ [/([ti])um$/i, '$1a'],
241
+ [/(buffal|tomat|potat|her)o$/i, '$1oes'],
242
+ [/(us|as)$/i, '$1es'],
243
+ [/s$/i, 's'],
244
+ ];
245
+ for (const [pattern, replacement] of rules) {
246
+ if (pattern.test(word)) {
247
+ return word.replace(pattern, replacement);
248
+ }
249
+ }
250
+ return `${word}s`;
251
+ }
252
+ /** Laravel's `Str::snake`: every capital starts a new word, so `APIKey` becomes `a_p_i_key`. */
253
+ function eloquentSnake(name) {
254
+ return name.replace(/(.)(?=[A-Z])/g, '$1_').toLowerCase();
255
+ }
256
+ /** Pluralizes the last word of a snake_case name, like Laravel's `Str::plural`. */
257
+ function pluralizeSnake(snakeName) {
258
+ const parts = snakeName.split('_');
259
+ const last = parts[parts.length - 1] ?? '';
260
+ parts[parts.length - 1] = pluralizeWord(last);
261
+ return parts.join('_');
262
+ }
263
+ /** The table Eloquent assumes for a model without `$table`: `UserProfile` becomes `user_profiles`. */
264
+ function conventionalTableName(className) {
265
+ return pluralizeSnake(eloquentSnake(className));
266
+ }
267
+ /** The name Laravel gives an index when the migration does not: `posts_title_index`. */
268
+ function conventionalIndexName(table, columns, type) {
269
+ return `${table}_${columns.join('_')}_${type}`
270
+ .toLowerCase()
271
+ .replace(/[-.]/g, '_');
272
+ }
273
+ /** Flattens `$a->b()->c()` or `A::b()->c()` into its root and calls. */
274
+ function readChain(expression) {
275
+ const links = [];
276
+ let current = expression;
277
+ for (;;) {
278
+ if (current.type === 'member_call_expression' ||
279
+ current.type === 'nullsafe_member_call_expression') {
280
+ const nameNode = current.childForFieldName('name');
281
+ const objectNode = current.childForFieldName('object');
282
+ if (nameNode === null || objectNode === null) {
283
+ return undefined;
284
+ }
285
+ links.unshift({
286
+ name: nameNode.text,
287
+ args: argumentNodes(current.childForFieldName('arguments')),
288
+ node: current,
289
+ });
290
+ current = objectNode;
291
+ }
292
+ else if (current.type === 'scoped_call_expression') {
293
+ const nameNode = current.childForFieldName('name');
294
+ const scopeNode = current.childForFieldName('scope');
295
+ if (nameNode === null || scopeNode === null) {
296
+ return undefined;
297
+ }
298
+ links.unshift({
299
+ name: nameNode.text,
300
+ args: argumentNodes(current.childForFieldName('arguments')),
301
+ node: current,
302
+ });
303
+ return { root: scopeNode, isStatic: true, links };
304
+ }
305
+ else if (current.type === 'parenthesized_expression') {
306
+ const inner = current.namedChildren[0];
307
+ if (inner === undefined) {
308
+ return undefined;
309
+ }
310
+ current = inner;
311
+ }
312
+ else {
313
+ return { root: current, isStatic: false, links };
314
+ }
315
+ }
316
+ }
317
+ function rootName(chain) {
318
+ return chain.root.text.replace(/\s+/g, '').replace(/^\\/, '');
319
+ }
320
+ function isFacade(chain, facade) {
321
+ const name = rootName(chain);
322
+ return (chain.isStatic &&
323
+ (name === facade || name === `Illuminate\\Support\\Facades\\${facade}`));
324
+ }
325
+ function evaluateArgument(node) {
326
+ if (node.type === 'scoped_call_expression') {
327
+ const chain = readChain(node);
328
+ const first = chain?.links[0];
329
+ if (chain !== undefined && first !== undefined) {
330
+ const scope = rootName(chain);
331
+ const lastScope = lastNameSegment(scope);
332
+ if (lastScope === 'DB' && first.name === 'raw') {
333
+ const sql = first.args[0]
334
+ ? evaluateArgument(first.args[0].node)
335
+ : undefined;
336
+ if (sql !== undefined && sql.kind === 'string') {
337
+ return { kind: 'raw', sql: sql.value };
338
+ }
339
+ return { kind: 'other', text: node.text };
340
+ }
341
+ if ((lastScope === 'Carbon' || lastScope === 'CarbonImmutable') &&
342
+ first.name === 'now') {
343
+ return { kind: 'now' };
344
+ }
345
+ }
346
+ }
347
+ if (node.type === 'function_call_expression') {
348
+ const functionNode = node.childForFieldName('function');
349
+ if (functionNode !== null && functionNode.text === 'now') {
350
+ return { kind: 'now' };
351
+ }
352
+ }
353
+ if (node.type === 'object_creation_expression') {
354
+ const evaluated = evaluateNode(node);
355
+ if (evaluated.kind === 'new' &&
356
+ lastNameSegment(evaluated.className) === 'Expression') {
357
+ const sql = evaluated.args[0];
358
+ if (sql !== undefined && sql.kind === 'string') {
359
+ return { kind: 'raw', sql: sql.value };
360
+ }
361
+ }
362
+ return evaluated;
363
+ }
364
+ return evaluateNode(node);
365
+ }
366
+ function argumentAt(args, index, name) {
367
+ if (name !== undefined) {
368
+ const named = args.find((candidate) => candidate.name === name);
369
+ if (named !== undefined) {
370
+ return named;
371
+ }
372
+ }
373
+ return args.filter((candidate) => candidate.name === undefined)[index];
374
+ }
375
+ /** Reads a string argument. `undefined` means absent; `null` means present but not a literal string. */
376
+ function stringArgument(args, index, name) {
377
+ const argument = argumentAt(args, index, name);
378
+ if (argument === undefined) {
379
+ return undefined;
380
+ }
381
+ const value = evaluateArgument(argument.node);
382
+ return value.kind === 'string' ? value.value : null;
383
+ }
384
+ /** Reads an integer argument (`undefined` when absent, `null` when not a literal number). */
385
+ function numberArgument(args, index, name) {
386
+ const argument = argumentAt(args, index, name);
387
+ if (argument === undefined) {
388
+ return undefined;
389
+ }
390
+ const value = evaluateArgument(argument.node);
391
+ return value.kind === 'number' ? value.value : null;
392
+ }
393
+ /** Reads a boolean argument (`undefined` when absent, `null` when not a literal boolean). */
394
+ function booleanArgument(args, index, name) {
395
+ const argument = argumentAt(args, index, name);
396
+ if (argument === undefined) {
397
+ return undefined;
398
+ }
399
+ const value = evaluateArgument(argument.node);
400
+ return value.kind === 'bool' ? value.value : null;
401
+ }
402
+ /** Reads a string or an array of strings. `null` when something in it is not a literal string. */
403
+ function stringListArgument(argument) {
404
+ if (argument === undefined) {
405
+ return undefined;
406
+ }
407
+ const value = evaluateArgument(argument.node);
408
+ if (value.kind === 'string') {
409
+ return [value.value];
410
+ }
411
+ if (value.kind === 'array') {
412
+ const items = [];
413
+ for (const item of value.items) {
414
+ if (item.value.kind !== 'string') {
415
+ return null;
416
+ }
417
+ items.push(item.value.value);
418
+ }
419
+ return items;
420
+ }
421
+ return null;
422
+ }
423
+ /** All string arguments of a call: `dropColumn('a', 'b')` and `dropColumn(['a', 'b'])` both give `['a', 'b']`. */
424
+ function allStringArguments(args) {
425
+ const names = [];
426
+ for (const argument of args) {
427
+ const list = stringListArgument(argument);
428
+ if (list === null || list === undefined) {
429
+ return null;
430
+ }
431
+ names.push(...list);
432
+ }
433
+ return names;
434
+ }
435
+ /** Fully qualified class name from `Foo::class` or a class-name string. */
436
+ function classReference(value, ctx, ownerFqn) {
437
+ if (value.kind === 'classRef') {
438
+ const lowered = value.name.toLowerCase();
439
+ return lowered === 'self' || lowered === 'static'
440
+ ? ownerFqn
441
+ : resolveClassName(value.name, ctx);
442
+ }
443
+ if (value.kind === 'string' && /^\\?[A-Za-z_][\w\\]*$/.test(value.value)) {
444
+ return resolveClassName(value.value, ctx);
445
+ }
446
+ return undefined;
447
+ }
448
+ function shorten(text, length = 60) {
449
+ const flat = text.replace(/\s+/g, ' ').trim();
450
+ return flat.length > length ? `${flat.slice(0, length - 3)}...` : flat;
451
+ }
452
+ // ---------------------------------------------------------------------------
453
+ // Migration replay
454
+ // ---------------------------------------------------------------------------
455
+ const NOW_SQL = /^(now\(\)|current_timestamp(\(\d*\))?|getdate\(\)|sysdate|systimestamp|localtimestamp(\(\d*\))?|datetime\('now'\)|\(datetime\('now'\)\))$/i;
456
+ const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|newid\(\)|newsequentialid\(\)|sys_guid\(\))$/i;
457
+ function newColumn(name, type, overrides = {}) {
458
+ return {
459
+ name,
460
+ type,
461
+ isNullable: false,
462
+ hasExplicitLength: false,
463
+ isAutoIncrement: false,
464
+ isAutoUpdated: false,
465
+ ...overrides,
466
+ };
467
+ }
468
+ function emptyTable(name, isPartial) {
469
+ return {
470
+ name,
471
+ columns: new Map(),
472
+ indexes: [],
473
+ foreignKeys: [],
474
+ primaryKey: undefined,
475
+ isPartial,
476
+ };
477
+ }
478
+ function actionFromText(text) {
479
+ return ON_DELETE_ACTIONS[text.trim().toLowerCase()];
480
+ }
481
+ function addIndex(table, definition) {
482
+ const existing = table.indexes.find((candidate) => sameList(candidate.columns, definition.columns) &&
483
+ candidate.isUnique === definition.isUnique &&
484
+ candidate.kind === definition.kind);
485
+ if (existing === undefined) {
486
+ table.indexes.push(definition);
487
+ }
488
+ else if (definition.name !== undefined) {
489
+ existing.name = definition.name;
490
+ }
491
+ }
492
+ function addForeignKey(table, definition) {
493
+ const position = table.foreignKeys.findIndex((candidate) => sameList(candidate.columns, definition.columns));
494
+ if (position >= 0) {
495
+ table.foreignKeys.splice(position, 1, definition);
496
+ }
497
+ else {
498
+ table.foreignKeys.push(definition);
499
+ }
500
+ }
501
+ function removeColumn(table, columnName) {
502
+ table.columns.delete(columnName);
503
+ table.indexes = table.indexes
504
+ .map((index) => ({
505
+ ...index,
506
+ columns: index.columns.filter((name) => name !== columnName),
507
+ }))
508
+ .filter((index) => index.columns.length > 0);
509
+ table.foreignKeys = table.foreignKeys.filter((key) => !key.columns.includes(columnName));
510
+ if (table.primaryKey?.includes(columnName) === true) {
511
+ const remaining = table.primaryKey.filter((name) => name !== columnName);
512
+ table.primaryKey = remaining.length > 0 ? remaining : undefined;
513
+ }
514
+ }
515
+ function renameColumn(table, from, to) {
516
+ const rebuilt = new Map();
517
+ for (const [name, column] of table.columns) {
518
+ if (name === from) {
519
+ rebuilt.set(to, { ...column, name: to });
520
+ }
521
+ else {
522
+ rebuilt.set(name, column);
523
+ }
524
+ }
525
+ table.columns = rebuilt;
526
+ const rename = (names) => names.map((name) => (name === from ? to : name));
527
+ for (const index of table.indexes) {
528
+ index.columns = rename(index.columns);
529
+ }
530
+ for (const key of table.foreignKeys) {
531
+ key.columns = rename(key.columns);
532
+ }
533
+ if (table.primaryKey !== undefined) {
534
+ table.primaryKey = rename(table.primaryKey);
535
+ }
536
+ }
537
+ /** Converts the argument of `default()` for a column; undefined means no default (or one that could not be read). */
538
+ function convertDefault(value, column, table, warnings) {
539
+ switch (value.kind) {
540
+ case 'null':
541
+ return undefined;
542
+ case 'bool':
543
+ return { kind: 'literal', value: value.value };
544
+ case 'number':
545
+ return column.type === 'string' || column.type === 'text'
546
+ ? { kind: 'literal', value: String(value.value) }
547
+ : { kind: 'literal', value: value.value };
548
+ case 'now':
549
+ return { kind: 'now' };
550
+ case 'string':
551
+ return convertTextDefault(value.value, column);
552
+ case 'raw': {
553
+ let sql = value.sql.trim();
554
+ while (sql.startsWith('(') && sql.endsWith(')')) {
555
+ sql = sql.slice(1, -1).trim();
556
+ }
557
+ if (NOW_SQL.test(sql)) {
558
+ return { kind: 'now' };
559
+ }
560
+ if (UUID_SQL.test(sql)) {
561
+ return { kind: 'uuid' };
562
+ }
563
+ if (/^-?\d+(\.\d+)?$/.test(sql)) {
564
+ return { kind: 'literal', value: Number(sql) };
565
+ }
566
+ if (/^(true|false)$/i.test(sql)) {
567
+ return { kind: 'literal', value: sql.toLowerCase() === 'true' };
568
+ }
569
+ const quoted = /^'((?:[^']|'')*)'$/.exec(sql);
570
+ if (quoted !== null) {
571
+ return convertTextDefault((quoted[1] ?? '').replace(/''/g, "'"), column);
572
+ }
573
+ return { kind: 'dbExpression', expression: sql };
574
+ }
575
+ default:
576
+ warnings.push(`${table}.${column.name}: the default value (${shorten(describeValue(value))}) cannot be evaluated statically and was ignored.`);
577
+ return undefined;
578
+ }
579
+ }
580
+ function describeValue(value) {
581
+ switch (value.kind) {
582
+ case 'other':
583
+ return value.text;
584
+ case 'array':
585
+ return 'an array';
586
+ case 'classRef':
587
+ return `${value.name}::class`;
588
+ case 'constant':
589
+ return `${value.owner}::${value.name}`;
590
+ default:
591
+ return value.kind;
592
+ }
593
+ }
594
+ function convertTextDefault(text, column) {
595
+ const trimmed = text.trim();
596
+ switch (column.type) {
597
+ case 'dateTime':
598
+ case 'date':
599
+ case 'time':
600
+ if (NOW_SQL.test(trimmed) || /^current_(date|time)$/i.test(trimmed)) {
601
+ return { kind: 'now' };
602
+ }
603
+ break;
604
+ case 'uuid':
605
+ case 'string':
606
+ if (UUID_SQL.test(trimmed)) {
607
+ return { kind: 'uuid' };
608
+ }
609
+ break;
610
+ case 'boolean':
611
+ if (/^(1|true)$/i.test(trimmed)) {
612
+ return { kind: 'literal', value: true };
613
+ }
614
+ if (/^(0|false)$/i.test(trimmed)) {
615
+ return { kind: 'literal', value: false };
616
+ }
617
+ break;
618
+ case 'int':
619
+ case 'bigInt':
620
+ case 'float':
621
+ case 'decimal':
622
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
623
+ return { kind: 'literal', value: Number(trimmed) };
624
+ }
625
+ break;
626
+ default:
627
+ break;
628
+ }
629
+ return { kind: 'literal', value: text };
630
+ }
631
+ const INTEGER_METHODS = {
632
+ integer: 'int',
633
+ tinyInteger: 'int',
634
+ smallInteger: 'int',
635
+ mediumInteger: 'int',
636
+ bigInteger: 'bigInt',
637
+ unsignedInteger: 'int',
638
+ unsignedTinyInteger: 'int',
639
+ unsignedSmallInteger: 'int',
640
+ unsignedMediumInteger: 'int',
641
+ unsignedBigInteger: 'bigInt',
642
+ };
643
+ const INCREMENT_METHODS = {
644
+ increments: 'int',
645
+ integerIncrements: 'int',
646
+ tinyIncrements: 'int',
647
+ smallIncrements: 'int',
648
+ mediumIncrements: 'int',
649
+ bigIncrements: 'bigInt',
650
+ };
651
+ const TEXT_METHODS = new Set([
652
+ 'text',
653
+ 'tinyText',
654
+ 'mediumText',
655
+ 'longText',
656
+ ]);
657
+ const DATE_METHODS = {
658
+ date: 'date',
659
+ dateTime: 'dateTime',
660
+ dateTimeTz: 'dateTime',
661
+ timestamp: 'dateTime',
662
+ timestampTz: 'dateTime',
663
+ time: 'time',
664
+ timeTz: 'time',
665
+ };
666
+ /** Methods that create a pair of `created_at` / `updated_at` columns. */
667
+ const TIMESTAMP_PAIR_METHODS = new Set([
668
+ 'timestamps',
669
+ 'timestampsTz',
670
+ 'nullableTimestamps',
671
+ 'datetimes',
672
+ 'datetimesTz',
673
+ ]);
674
+ const SOFT_DELETE_METHODS = new Set([
675
+ 'softDeletes',
676
+ 'softDeletesTz',
677
+ 'softDeletesDatetime',
678
+ ]);
679
+ /** `morphs`-style methods: [type of the id column, nullable]. */
680
+ const MORPH_METHODS = {
681
+ morphs: { id: 'int', nullable: false },
682
+ nullableMorphs: { id: 'int', nullable: true },
683
+ uuidMorphs: { id: 'uuid', nullable: false },
684
+ nullableUuidMorphs: { id: 'uuid', nullable: true },
685
+ ulidMorphs: { id: 'ulid', nullable: false },
686
+ nullableUlidMorphs: { id: 'ulid', nullable: true },
687
+ };
688
+ /** Looks at the first call of a Blueprint statement and creates the columns or runs the table command. */
689
+ function startCall(table, first, mctx) {
690
+ const warnings = mctx.state.warnings;
691
+ const method = first.name;
692
+ const args = first.args;
693
+ const skip = (what) => {
694
+ pushWarning(warnings, `${table.name}: ${method}() ${what}; the call was skipped.`);
695
+ return undefined;
696
+ };
697
+ /** The column name argument, or the fallback; null when it is not a literal. */
698
+ const columnName = (fallback) => {
699
+ const given = stringArgument(args, 0, 'column');
700
+ if (given === undefined) {
701
+ return fallback ?? null;
702
+ }
703
+ return given;
704
+ };
705
+ const single = (type, fallback, overrides = {}) => {
706
+ const name = columnName(fallback);
707
+ if (name === null) {
708
+ return skip('has a column name that is not a string literal');
709
+ }
710
+ return {
711
+ columns: [newColumn(name, type, overrides)],
712
+ change: false,
713
+ };
714
+ };
715
+ const primaryTarget = (type, fallback) => {
716
+ const target = single(type, fallback, {
717
+ isAutoIncrement: true,
718
+ default: { kind: 'autoIncrement' },
719
+ });
720
+ const created = target?.columns[0];
721
+ if (created !== undefined) {
722
+ table.primaryKey = [created.name];
723
+ }
724
+ return target;
725
+ };
726
+ // Columns --------------------------------------------------------------
727
+ if (method === 'id') {
728
+ return primaryTarget('bigInt', 'id');
729
+ }
730
+ const incrementType = INCREMENT_METHODS[method];
731
+ if (incrementType !== undefined) {
732
+ return primaryTarget(incrementType, undefined);
733
+ }
734
+ const integerType = INTEGER_METHODS[method];
735
+ if (integerType !== undefined) {
736
+ const autoIncrement = booleanArgument(args, 1, 'autoIncrement') === true;
737
+ return autoIncrement
738
+ ? primaryTarget(integerType, undefined)
739
+ : single(integerType, undefined);
740
+ }
741
+ if (method === 'string' || method === 'char') {
742
+ const length = numberArgument(args, 1, 'length');
743
+ if (length === null) {
744
+ pushWarning(warnings, `${table.name}: the length of ${method}() is not a number literal; the default length 255 was used.`);
745
+ }
746
+ return single('string', undefined, {
747
+ maxLength: typeof length === 'number' ? length : 255,
748
+ hasExplicitLength: typeof length === 'number',
749
+ });
750
+ }
751
+ if (TEXT_METHODS.has(method)) {
752
+ return single('text', undefined);
753
+ }
754
+ if (method === 'boolean') {
755
+ return single('boolean', undefined);
756
+ }
757
+ if (method === 'decimal' || method === 'unsignedDecimal') {
758
+ const total = numberArgument(args, 1, 'total');
759
+ const places = numberArgument(args, 2, 'places');
760
+ return single('decimal', undefined, {
761
+ maxDigits: typeof total === 'number' ? total : 8,
762
+ decimalPlaces: typeof places === 'number' ? places : 2,
763
+ });
764
+ }
765
+ if (method === 'float' || method === 'double') {
766
+ return single('float', undefined);
767
+ }
768
+ const dateType = DATE_METHODS[method];
769
+ if (dateType !== undefined) {
770
+ return single(dateType, undefined);
771
+ }
772
+ if (method === 'year') {
773
+ return single('int', undefined);
774
+ }
775
+ if (TIMESTAMP_PAIR_METHODS.has(method)) {
776
+ return {
777
+ columns: [
778
+ newColumn('created_at', 'dateTime', { isNullable: true }),
779
+ newColumn('updated_at', 'dateTime', { isNullable: true }),
780
+ ],
781
+ change: false,
782
+ };
783
+ }
784
+ if (SOFT_DELETE_METHODS.has(method)) {
785
+ return single('dateTime', 'deleted_at', { isNullable: true });
786
+ }
787
+ if (method === 'rememberToken') {
788
+ return {
789
+ columns: [
790
+ newColumn('remember_token', 'string', {
791
+ isNullable: true,
792
+ maxLength: 100,
793
+ hasExplicitLength: true,
794
+ }),
795
+ ],
796
+ change: false,
797
+ };
798
+ }
799
+ if (method === 'uuid') {
800
+ return single('uuid', 'uuid');
801
+ }
802
+ if (method === 'ulid') {
803
+ const length = numberArgument(args, 1, 'length');
804
+ return single('string', 'ulid', {
805
+ maxLength: typeof length === 'number' ? length : 26,
806
+ hasExplicitLength: true,
807
+ });
808
+ }
809
+ if (method === 'json' || method === 'jsonb') {
810
+ return single('json', undefined);
811
+ }
812
+ if (method === 'binary') {
813
+ return single('bytes', undefined);
814
+ }
815
+ if (method === 'ipAddress') {
816
+ return single('ipAddress', 'ip_address');
817
+ }
818
+ if (method === 'macAddress') {
819
+ const target = single('string', 'mac_address', {
820
+ maxLength: 17,
821
+ hasExplicitLength: true,
822
+ });
823
+ if (target !== undefined) {
824
+ pushWarning(warnings, `${table.name}.${target.columns[0]?.name ?? 'mac_address'}: macAddress() has no equivalent and was converted to a string column of length 17.`);
825
+ }
826
+ return target;
827
+ }
828
+ if (method === 'enum' || method === 'set') {
829
+ const values = stringListArgument(argumentAt(args, 1, 'allowed'));
830
+ const name = columnName(undefined);
831
+ if (name === null) {
832
+ return skip('has a column name that is not a string literal');
833
+ }
834
+ if (values === null || values === undefined) {
835
+ return skip('has allowed values that are not string literals');
836
+ }
837
+ if (method === 'set') {
838
+ pushWarning(warnings, `${table.name}.${name}: set() columns have no equivalent and were kept as unsupported columns.`);
839
+ return {
840
+ columns: [
841
+ newColumn(name, 'unsupported', {
842
+ unsupportedType: `set(${values.map((value) => `'${value}'`).join(',')})`,
843
+ }),
844
+ ],
845
+ change: false,
846
+ };
847
+ }
848
+ return {
849
+ columns: [newColumn(name, 'string', { enumValues: values })],
850
+ change: false,
851
+ };
852
+ }
853
+ if (GEOMETRY_TYPES.has(method) || method === 'vector') {
854
+ const target = single('unsupported', undefined, {
855
+ unsupportedType: method.toLowerCase(),
856
+ });
857
+ if (target !== undefined) {
858
+ pushWarning(warnings, `${table.name}.${target.columns[0]?.name ?? method}: the ${method}() column type has no equivalent and was kept as an unsupported column.`);
859
+ }
860
+ return target;
861
+ }
862
+ if (method === 'foreignId' ||
863
+ method === 'foreignUuid' ||
864
+ method === 'foreignUlid') {
865
+ const type = method === 'foreignId'
866
+ ? 'bigInt'
867
+ : method === 'foreignUuid'
868
+ ? 'uuid'
869
+ : 'string';
870
+ const target = single(type, undefined, method === 'foreignUlid' ? { maxLength: 26, hasExplicitLength: true } : {});
871
+ if (target !== undefined) {
872
+ target.foreignIdColumn = target.columns[0];
873
+ }
874
+ return target;
875
+ }
876
+ if (method === 'foreignIdFor') {
877
+ const model = args[0]
878
+ ? evaluateArgument(args[0].node)
879
+ : undefined;
880
+ const modelName = model !== undefined && model.kind === 'classRef'
881
+ ? model.name
882
+ : model !== undefined && model.kind === 'string'
883
+ ? model.value
884
+ : undefined;
885
+ if (modelName === undefined) {
886
+ return skip('was not given a model class');
887
+ }
888
+ const explicit = stringArgument(args, 1, 'column');
889
+ const name = explicit ?? `${eloquentSnake(lastNameSegment(modelName))}_id`;
890
+ const column = newColumn(name, 'bigInt');
891
+ return {
892
+ columns: [column],
893
+ foreignIdColumn: column,
894
+ foreignIdModel: modelName,
895
+ change: false,
896
+ };
897
+ }
898
+ const morph = MORPH_METHODS[method];
899
+ if (morph !== undefined) {
900
+ const name = columnName(undefined);
901
+ if (name === null) {
902
+ return skip('has a name that is not a string literal');
903
+ }
904
+ const typeColumn = newColumn(`${name}_type`, 'string', {
905
+ maxLength: 255,
906
+ hasExplicitLength: true,
907
+ isNullable: morph.nullable,
908
+ });
909
+ const idColumn = morph.id === 'int'
910
+ ? newColumn(`${name}_id`, 'bigInt', { isNullable: morph.nullable })
911
+ : morph.id === 'uuid'
912
+ ? newColumn(`${name}_id`, 'uuid', { isNullable: morph.nullable })
913
+ : newColumn(`${name}_id`, 'string', {
914
+ maxLength: 26,
915
+ hasExplicitLength: true,
916
+ isNullable: morph.nullable,
917
+ });
918
+ const indexName = stringArgument(args, 1, 'indexName');
919
+ addIndex(table, {
920
+ columns: [typeColumn.name, idColumn.name],
921
+ isUnique: false,
922
+ ...(typeof indexName === 'string' ? { name: indexName } : {}),
923
+ });
924
+ pushWarning(warnings, `${table.name}: ${method}('${name}') creates the columns ${typeColumn.name} and ${idColumn.name}; polymorphic relations are not represented, so they were kept as plain columns.`);
925
+ return { columns: [typeColumn, idColumn], change: false };
926
+ }
927
+ // Indexes and keys -------------------------------------------------------
928
+ if (method === 'index' ||
929
+ method === 'unique' ||
930
+ method === 'primary' ||
931
+ method === 'fullText' ||
932
+ method === 'spatialIndex') {
933
+ const columns = stringListArgument(argumentAt(args, 0, 'columns'));
934
+ if (columns === null || columns === undefined) {
935
+ return skip('has columns that are not string literals');
936
+ }
937
+ if (method === 'spatialIndex') {
938
+ pushWarning(warnings, `${table.name}: the spatial index on (${columns.join(', ')}) has no equivalent and was ignored.`);
939
+ return undefined;
940
+ }
941
+ if (method === 'primary') {
942
+ table.primaryKey = columns;
943
+ return undefined;
944
+ }
945
+ const name = stringArgument(args, 1, 'name');
946
+ addIndex(table, {
947
+ columns,
948
+ isUnique: method === 'unique',
949
+ ...(method === 'fullText' ? { kind: 'fulltext' } : {}),
950
+ ...(typeof name === 'string' ? { name } : {}),
951
+ });
952
+ return undefined;
953
+ }
954
+ if (method === 'foreign') {
955
+ const columns = stringListArgument(argumentAt(args, 0, 'columns'));
956
+ if (columns === null || columns === undefined) {
957
+ return skip('has columns that are not string literals');
958
+ }
959
+ const name = stringArgument(args, 1, 'name');
960
+ const foreignKey = {
961
+ columns,
962
+ refColumns: [],
963
+ ...(typeof name === 'string' ? { name } : {}),
964
+ };
965
+ addForeignKey(table, foreignKey);
966
+ return { columns: [], foreignKey, change: false };
967
+ }
968
+ // Changes to existing columns and keys ----------------------------------
969
+ return runDropOrRename(table, first, mctx);
970
+ }
971
+ /** `dropColumn`, `renameColumn`, `dropIndex` and friends. */
972
+ function runDropOrRename(table, first, mctx) {
973
+ const warnings = mctx.state.warnings;
974
+ const method = first.name;
975
+ const args = first.args;
976
+ const missing = (what) => {
977
+ pushWarning(warnings, `${table.name}: ${method}() ${what}; the call was skipped.`);
978
+ };
979
+ switch (method) {
980
+ case 'dropColumn':
981
+ case 'dropColumns': {
982
+ const names = allStringArguments(args);
983
+ if (names === null) {
984
+ missing('has column names that are not string literals');
985
+ return undefined;
986
+ }
987
+ for (const name of names) {
988
+ removeColumn(table, name);
989
+ }
990
+ return undefined;
991
+ }
992
+ case 'renameColumn': {
993
+ const from = stringArgument(args, 0, 'from');
994
+ const to = stringArgument(args, 1, 'to');
995
+ if (typeof from !== 'string' || typeof to !== 'string') {
996
+ missing('has names that are not string literals');
997
+ }
998
+ else if (!table.columns.has(from)) {
999
+ if (!table.isPartial) {
1000
+ pushWarning(warnings, `${table.name}.${from}: renameColumn() refers to a column that does not exist at this point; the call was skipped.`);
1001
+ }
1002
+ }
1003
+ else {
1004
+ renameColumn(table, from, to);
1005
+ }
1006
+ return undefined;
1007
+ }
1008
+ case 'dropTimestamps':
1009
+ case 'dropTimestampsTz':
1010
+ removeColumn(table, 'created_at');
1011
+ removeColumn(table, 'updated_at');
1012
+ return undefined;
1013
+ case 'dropSoftDeletes':
1014
+ case 'dropSoftDeletesTz': {
1015
+ const name = stringArgument(args, 0, 'column');
1016
+ removeColumn(table, typeof name === 'string' ? name : 'deleted_at');
1017
+ return undefined;
1018
+ }
1019
+ case 'dropRememberToken':
1020
+ removeColumn(table, 'remember_token');
1021
+ return undefined;
1022
+ case 'dropMorphs': {
1023
+ const name = stringArgument(args, 0, 'name');
1024
+ if (typeof name !== 'string') {
1025
+ missing('has a name that is not a string literal');
1026
+ }
1027
+ else {
1028
+ removeColumn(table, `${name}_type`);
1029
+ removeColumn(table, `${name}_id`);
1030
+ }
1031
+ return undefined;
1032
+ }
1033
+ case 'dropConstrainedForeignId':
1034
+ case 'dropForeignIdFor':
1035
+ case 'dropConstrainedForeignIdFor': {
1036
+ const nameArg = args[0]
1037
+ ? evaluateArgument(args[0].node)
1038
+ : undefined;
1039
+ const column = nameArg === undefined
1040
+ ? undefined
1041
+ : nameArg.kind === 'string' && method === 'dropConstrainedForeignId'
1042
+ ? nameArg.value
1043
+ : nameArg.kind === 'classRef'
1044
+ ? `${eloquentSnake(lastNameSegment(nameArg.name))}_id`
1045
+ : undefined;
1046
+ if (column === undefined) {
1047
+ missing('was not given a column or model it can resolve');
1048
+ }
1049
+ else {
1050
+ removeColumn(table, column);
1051
+ }
1052
+ return undefined;
1053
+ }
1054
+ case 'dropPrimary':
1055
+ table.primaryKey = undefined;
1056
+ return undefined;
1057
+ case 'dropIndex':
1058
+ case 'dropUnique':
1059
+ case 'dropFullText':
1060
+ case 'dropSpatialIndex': {
1061
+ const target = stringListArgument(args[0]);
1062
+ if (target === null || target === undefined) {
1063
+ missing('has an index name or columns that are not string literals');
1064
+ return undefined;
1065
+ }
1066
+ const type = method === 'dropUnique'
1067
+ ? 'unique'
1068
+ : method === 'dropFullText'
1069
+ ? 'fulltext'
1070
+ : 'index';
1071
+ const isColumnList = args[0] !== undefined &&
1072
+ evaluateArgument(args[0].node).kind === 'array';
1073
+ const wanted = isColumnList
1074
+ ? conventionalIndexName(table.name, target, type)
1075
+ : (target[0] ?? '');
1076
+ const before = table.indexes.length;
1077
+ table.indexes = table.indexes.filter((index) => {
1078
+ const indexType = index.kind === 'fulltext'
1079
+ ? 'fulltext'
1080
+ : index.isUnique
1081
+ ? 'unique'
1082
+ : 'index';
1083
+ const name = index.name ??
1084
+ conventionalIndexName(table.name, index.columns, indexType);
1085
+ return name !== wanted;
1086
+ });
1087
+ if (table.indexes.length === before && !table.isPartial) {
1088
+ pushWarning(warnings, `${table.name}: ${method}() could not find the index "${wanted}"; the call was skipped.`);
1089
+ }
1090
+ return undefined;
1091
+ }
1092
+ case 'dropForeign': {
1093
+ const target = stringListArgument(args[0]);
1094
+ if (target === null || target === undefined) {
1095
+ missing('has a constraint name or columns that are not string literals');
1096
+ return undefined;
1097
+ }
1098
+ const isColumnList = args[0] !== undefined &&
1099
+ evaluateArgument(args[0].node).kind === 'array';
1100
+ const before = table.foreignKeys.length;
1101
+ table.foreignKeys = table.foreignKeys.filter((key) => isColumnList
1102
+ ? !sameList(key.columns, target)
1103
+ : (key.name ??
1104
+ conventionalIndexName(table.name, key.columns, 'foreign')) !==
1105
+ target[0]);
1106
+ if (table.foreignKeys.length === before && !table.isPartial) {
1107
+ pushWarning(warnings, `${table.name}: dropForeign() could not find the foreign key ${target.join(', ')}; the call was skipped.`);
1108
+ }
1109
+ return undefined;
1110
+ }
1111
+ case 'renameIndex': {
1112
+ const from = stringArgument(args, 0, 'from');
1113
+ const to = stringArgument(args, 1, 'to');
1114
+ if (typeof from !== 'string' || typeof to !== 'string') {
1115
+ missing('has names that are not string literals');
1116
+ return undefined;
1117
+ }
1118
+ const index = table.indexes.find((candidate) => (candidate.name ??
1119
+ conventionalIndexName(table.name, candidate.columns, candidate.kind === 'fulltext'
1120
+ ? 'fulltext'
1121
+ : candidate.isUnique
1122
+ ? 'unique'
1123
+ : 'index')) === from);
1124
+ if (index !== undefined) {
1125
+ index.name = to;
1126
+ }
1127
+ return undefined;
1128
+ }
1129
+ default:
1130
+ if (IGNORED_TABLE_METHODS.has(method)) {
1131
+ return undefined;
1132
+ }
1133
+ pushWarning(warnings, `${table.name}: the Blueprint call ${method}() is not understood and was ignored.`);
1134
+ return undefined;
1135
+ }
1136
+ }
1137
+ /** Applies the calls chained after a column definition (`->nullable()->default(0)->unique()`). */
1138
+ function applyModifiers(table, target, links, mctx) {
1139
+ const warnings = mctx.state.warnings;
1140
+ for (const link of links) {
1141
+ const name = link.name;
1142
+ const label = target.columns.length === 1 && target.columns[0] !== undefined
1143
+ ? `${table.name}.${target.columns[0].name}`
1144
+ : table.name;
1145
+ const setAction = (field, action) => {
1146
+ if (target.foreignKey === undefined) {
1147
+ pushWarning(warnings, `${label}: ${name}() was used without a foreign key definition and was ignored.`);
1148
+ }
1149
+ else {
1150
+ target.foreignKey[field] = action;
1151
+ }
1152
+ };
1153
+ switch (name) {
1154
+ case 'nullable': {
1155
+ const flag = booleanArgument(link.args, 0, 'value');
1156
+ for (const column of target.columns) {
1157
+ column.isNullable = flag !== false;
1158
+ }
1159
+ break;
1160
+ }
1161
+ case 'default': {
1162
+ const argument = argumentAt(link.args, 0, 'value');
1163
+ for (const column of target.columns) {
1164
+ if (argument === undefined) {
1165
+ continue;
1166
+ }
1167
+ const converted = convertDefault(evaluateArgument(argument.node), column, table.name, warnings);
1168
+ if (converted === undefined) {
1169
+ delete column.default;
1170
+ }
1171
+ else {
1172
+ column.default = converted;
1173
+ }
1174
+ }
1175
+ break;
1176
+ }
1177
+ case 'useCurrent':
1178
+ for (const column of target.columns) {
1179
+ column.default = { kind: 'now' };
1180
+ }
1181
+ break;
1182
+ case 'useCurrentOnUpdate':
1183
+ for (const column of target.columns) {
1184
+ column.isAutoUpdated = true;
1185
+ }
1186
+ break;
1187
+ case 'autoIncrement':
1188
+ for (const column of target.columns) {
1189
+ column.isAutoIncrement = true;
1190
+ column.default = { kind: 'autoIncrement' };
1191
+ }
1192
+ break;
1193
+ case 'unique':
1194
+ case 'index':
1195
+ case 'fullText':
1196
+ case 'primary':
1197
+ case 'spatialIndex': {
1198
+ const indexName = stringArgument(link.args, 0, 'indexName');
1199
+ for (const column of target.columns) {
1200
+ if (name === 'primary') {
1201
+ table.primaryKey = [column.name];
1202
+ }
1203
+ else if (name === 'spatialIndex') {
1204
+ pushWarning(warnings, `${table.name}.${column.name}: the spatial index has no equivalent and was ignored.`);
1205
+ }
1206
+ else {
1207
+ addIndex(table, {
1208
+ columns: [column.name],
1209
+ isUnique: name === 'unique',
1210
+ ...(name === 'fullText' ? { kind: 'fulltext' } : {}),
1211
+ ...(typeof indexName === 'string' ? { name: indexName } : {}),
1212
+ });
1213
+ }
1214
+ }
1215
+ break;
1216
+ }
1217
+ case 'storedAs':
1218
+ case 'virtualAs': {
1219
+ const expression = link.args[0]
1220
+ ? evaluateArgument(link.args[0].node)
1221
+ : undefined;
1222
+ const text = expression === undefined
1223
+ ? undefined
1224
+ : expression.kind === 'string'
1225
+ ? expression.value
1226
+ : expression.kind === 'raw'
1227
+ ? expression.sql
1228
+ : undefined;
1229
+ for (const column of target.columns) {
1230
+ if (text === undefined) {
1231
+ pushWarning(warnings, `${table.name}.${column.name}: the expression of ${name}() is not a string literal and was ignored.`);
1232
+ }
1233
+ else {
1234
+ column.generated = {
1235
+ expression: text,
1236
+ isStored: name === 'storedAs',
1237
+ };
1238
+ }
1239
+ }
1240
+ break;
1241
+ }
1242
+ case 'generatedAs':
1243
+ case 'always':
1244
+ pushWarning(warnings, `${label}: identity columns (${name}()) have no equivalent and were ignored.`);
1245
+ break;
1246
+ case 'change':
1247
+ target.change = true;
1248
+ break;
1249
+ case 'constrained': {
1250
+ const column = target.foreignIdColumn;
1251
+ if (column === undefined) {
1252
+ pushWarning(warnings, `${label}: constrained() can only follow foreignId(), foreignUuid(), foreignUlid() or foreignIdFor(); it was ignored.`);
1253
+ break;
1254
+ }
1255
+ const tableArgument = stringArgument(link.args, 0, 'table');
1256
+ const columnArgument = stringArgument(link.args, 1, 'column');
1257
+ const indexName = stringArgument(link.args, 2, 'indexName');
1258
+ const foreignKey = {
1259
+ columns: [column.name],
1260
+ refColumns: [
1261
+ typeof columnArgument === 'string' ? columnArgument : 'id',
1262
+ ],
1263
+ ...(typeof tableArgument === 'string'
1264
+ ? { refTable: tableArgument }
1265
+ : target.foreignIdModel !== undefined
1266
+ ? { refModel: target.foreignIdModel }
1267
+ : { refTable: pluralizeSnake(column.name.replace(/_id$/, '')) }),
1268
+ ...(typeof indexName === 'string' ? { name: indexName } : {}),
1269
+ };
1270
+ target.foreignKey = foreignKey;
1271
+ addForeignKey(table, foreignKey);
1272
+ break;
1273
+ }
1274
+ case 'references': {
1275
+ const columns = stringListArgument(argumentAt(link.args, 0, 'columns'));
1276
+ const own = target.columns.map((column) => column.name);
1277
+ if (target.foreignKey === undefined) {
1278
+ if (own.length === 0) {
1279
+ pushWarning(warnings, `${label}: references() was used without a foreign key definition and was ignored.`);
1280
+ break;
1281
+ }
1282
+ target.foreignKey = { columns: own, refColumns: [] };
1283
+ addForeignKey(table, target.foreignKey);
1284
+ }
1285
+ if (columns === null || columns === undefined) {
1286
+ pushWarning(warnings, `${label}: references() was given columns that are not string literals; 'id' was assumed.`);
1287
+ target.foreignKey.refColumns = ['id'];
1288
+ }
1289
+ else {
1290
+ target.foreignKey.refColumns = columns;
1291
+ }
1292
+ break;
1293
+ }
1294
+ case 'on': {
1295
+ const referenced = stringArgument(link.args, 0, 'table');
1296
+ if (target.foreignKey === undefined || typeof referenced !== 'string') {
1297
+ pushWarning(warnings, `${label}: on() ${target.foreignKey === undefined ? 'was used without a foreign key definition' : 'was given a table that is not a string literal'} and was ignored.`);
1298
+ }
1299
+ else {
1300
+ target.foreignKey.refTable = referenced;
1301
+ }
1302
+ break;
1303
+ }
1304
+ case 'onDelete':
1305
+ case 'onUpdate': {
1306
+ const text = stringArgument(link.args, 0, 'action');
1307
+ const action = typeof text === 'string' ? actionFromText(text) : undefined;
1308
+ if (action === undefined) {
1309
+ pushWarning(warnings, `${label}: ${name}(${text === undefined || text === null ? '...' : `'${text}'`}) is not a supported referential action and was ignored.`);
1310
+ }
1311
+ else {
1312
+ setAction(name, action);
1313
+ }
1314
+ break;
1315
+ }
1316
+ case 'cascadeOnDelete':
1317
+ setAction('onDelete', 'cascade');
1318
+ break;
1319
+ case 'restrictOnDelete':
1320
+ setAction('onDelete', 'restrict');
1321
+ break;
1322
+ case 'nullOnDelete':
1323
+ setAction('onDelete', 'setNull');
1324
+ break;
1325
+ case 'noActionOnDelete':
1326
+ setAction('onDelete', 'noAction');
1327
+ break;
1328
+ case 'cascadeOnUpdate':
1329
+ setAction('onUpdate', 'cascade');
1330
+ break;
1331
+ case 'restrictOnUpdate':
1332
+ setAction('onUpdate', 'restrict');
1333
+ break;
1334
+ case 'nullOnUpdate':
1335
+ setAction('onUpdate', 'setNull');
1336
+ break;
1337
+ case 'noActionOnUpdate':
1338
+ setAction('onUpdate', 'noAction');
1339
+ break;
1340
+ default:
1341
+ if (!IGNORED_COLUMN_MODIFIERS.has(name)) {
1342
+ pushWarning(warnings, `${label}: the column modifier ${name}() is not understood and was ignored.`);
1343
+ }
1344
+ break;
1345
+ }
1346
+ }
1347
+ }
1348
+ function commitColumns(table, target, mctx) {
1349
+ for (const column of target.columns) {
1350
+ const existing = table.columns.get(column.name);
1351
+ if (target.change) {
1352
+ if (existing === undefined && !table.isPartial) {
1353
+ pushWarning(mctx.state.warnings, `${table.name}.${column.name}: change() modifies a column that does not exist at this point; it was added instead.`);
1354
+ }
1355
+ }
1356
+ else if (existing !== undefined && !table.isPartial) {
1357
+ pushWarning(mctx.state.warnings, `${table.name}.${column.name}: the column is defined more than once; the later definition replaced the earlier one.`);
1358
+ }
1359
+ table.columns.set(column.name, column);
1360
+ }
1361
+ }
1362
+ function runTableCall(table, links, mctx) {
1363
+ const first = links[0];
1364
+ if (first === undefined) {
1365
+ return;
1366
+ }
1367
+ mctx.touched.push(table.name);
1368
+ const target = startCall(table, first, mctx);
1369
+ if (target === undefined) {
1370
+ return;
1371
+ }
1372
+ applyModifiers(table, target, links.slice(1), mctx);
1373
+ commitColumns(table, target, mctx);
1374
+ }
1375
+ // -- Statement walking --------------------------------------------------------
1376
+ function nestedStatements(node) {
1377
+ const found = [];
1378
+ for (const child of node.namedChildren) {
1379
+ if (CLAUSE_STATEMENTS.has(child.type)) {
1380
+ found.push(...nestedStatements(child));
1381
+ }
1382
+ else if (child.type === 'compound_statement' ||
1383
+ child.type.endsWith('_statement')) {
1384
+ found.push(child);
1385
+ }
1386
+ }
1387
+ return found;
1388
+ }
1389
+ function controlLabel(node) {
1390
+ switch (node.type) {
1391
+ case 'if_statement':
1392
+ return 'a conditional';
1393
+ case 'switch_statement':
1394
+ return 'a switch';
1395
+ case 'try_statement':
1396
+ return 'a try block';
1397
+ default:
1398
+ return 'a loop';
1399
+ }
1400
+ }
1401
+ function forEachStatement(nodes, handle, onControl) {
1402
+ for (const node of nodes) {
1403
+ if (node.type === 'comment') {
1404
+ continue;
1405
+ }
1406
+ if (node.type === 'compound_statement') {
1407
+ forEachStatement(node.namedChildren, handle, onControl);
1408
+ }
1409
+ else if (CONTROL_STATEMENTS.has(node.type)) {
1410
+ onControl(node, () => forEachStatement(nestedStatements(node), handle, onControl));
1411
+ }
1412
+ else {
1413
+ handle(node);
1414
+ }
1415
+ }
1416
+ }
1417
+ /** The expression of an expression statement (or the node itself for an arrow function body). */
1418
+ function statementExpression(statement) {
1419
+ if (statement.type === 'expression_statement') {
1420
+ return statement.namedChildren.find((child) => child.type !== 'comment');
1421
+ }
1422
+ return statement.type.endsWith('_expression') ? statement : undefined;
1423
+ }
1424
+ function warnRawSql(chain, mctx, statementText) {
1425
+ const first = chain.links[0];
1426
+ if (first === undefined || !RAW_SQL_METHODS.has(first.name)) {
1427
+ return;
1428
+ }
1429
+ const sqlArgument = first.args[0]
1430
+ ? evaluateArgument(first.args[0].node)
1431
+ : undefined;
1432
+ const sql = sqlArgument !== undefined && sqlArgument.kind === 'string'
1433
+ ? sqlArgument.value
1434
+ : undefined;
1435
+ const tableMatch = sql === undefined
1436
+ ? null
1437
+ : /\b(?:alter\s+table|create\s+table(?:\s+if\s+not\s+exists)?|drop\s+table(?:\s+if\s+exists)?|truncate(?:\s+table)?|rename\s+table|create\s+(?:unique\s+)?index\s+\S+\s+on|drop\s+index\s+\S+\s+on|comment\s+on\s+(?:table|column))\s+[`"']?([A-Za-z0-9_.]+)/i.exec(sql);
1438
+ const tableName = tableMatch?.[1];
1439
+ pushWarning(mctx.state.warnings, `${mctx.file}: the raw SQL statement "${shorten(sql ?? statementText)}"${tableName === undefined ? '' : ` on the table "${tableName}"`} is not interpreted; the rebuilt schema may differ from the real database.`);
1440
+ if (tableName !== undefined) {
1441
+ mctx.touched.push(tableName);
1442
+ }
1443
+ }
1444
+ function runBlueprint(argument, table, mctx) {
1445
+ if (argument === undefined) {
1446
+ pushWarning(mctx.state.warnings, `${table.name}: no Blueprint callback was found; the table has no columns.`);
1447
+ return;
1448
+ }
1449
+ const closure = argument.node;
1450
+ if (closure.type !== 'anonymous_function_creation_expression' &&
1451
+ closure.type !== 'anonymous_function' &&
1452
+ closure.type !== 'arrow_function') {
1453
+ pushWarning(mctx.state.warnings, `${table.name}: the Blueprint callback is not an inline closure, so its columns could not be read.`);
1454
+ return;
1455
+ }
1456
+ const parameter = closure
1457
+ .childForFieldName('parameters')
1458
+ ?.namedChildren.find((child) => child.type === 'simple_parameter');
1459
+ const variable = parameter?.childForFieldName('name')?.text ?? '$table';
1460
+ const body = closure.childForFieldName('body');
1461
+ if (body === null) {
1462
+ return;
1463
+ }
1464
+ const statements = body.type === 'compound_statement' ? body.namedChildren : [body];
1465
+ forEachStatement(statements, (statement) => {
1466
+ const expression = statementExpression(statement);
1467
+ if (expression === undefined) {
1468
+ return;
1469
+ }
1470
+ const chain = readChain(expression);
1471
+ if (chain === undefined) {
1472
+ return;
1473
+ }
1474
+ if (!chain.isStatic && chain.root.text === variable) {
1475
+ runTableCall(table, chain.links, mctx);
1476
+ }
1477
+ else if (isFacade(chain, 'DB')) {
1478
+ warnRawSql(chain, mctx, statement.text);
1479
+ }
1480
+ }, (statement, descend) => describeControlFlow(statement, descend, mctx));
1481
+ }
1482
+ function describeControlFlow(statement, descend, mctx) {
1483
+ const before = mctx.touched.length;
1484
+ descend();
1485
+ const tables = unique(mctx.touched.slice(before));
1486
+ pushWarning(mctx.state.warnings, `${mctx.file}: ${controlLabel(statement)} contains schema changes; it was applied as if it always ran` +
1487
+ (tables.length > 0
1488
+ ? ` (tables: ${tables.join(', ')}), so the result may differ from the real database.`
1489
+ : ', so the result may differ from the real database.'));
1490
+ }
1491
+ function runSchemaCall(chain, mctx) {
1492
+ const state = mctx.state;
1493
+ const links = chain.links[0]?.name === 'connection' ? chain.links.slice(1) : chain.links;
1494
+ const call = links[0];
1495
+ if (call === undefined) {
1496
+ return;
1497
+ }
1498
+ const method = call.name;
1499
+ const tableNameOf = () => {
1500
+ const name = stringArgument(call.args, 0, 'table');
1501
+ if (typeof name !== 'string') {
1502
+ pushWarning(state.warnings, `${mctx.file}: Schema::${method}() was given a table name that is not a string literal; the call was skipped.`);
1503
+ return undefined;
1504
+ }
1505
+ return name;
1506
+ };
1507
+ switch (method) {
1508
+ case 'create':
1509
+ case 'table': {
1510
+ const name = tableNameOf();
1511
+ if (name === undefined) {
1512
+ return;
1513
+ }
1514
+ let table = state.tables.get(name);
1515
+ if (method === 'create') {
1516
+ if (table !== undefined && !table.isPartial) {
1517
+ pushWarning(state.warnings, `${name}: Schema::create() ran for a table that already exists (in ${mctx.file}); the earlier definition was replaced.`);
1518
+ }
1519
+ state.tables.delete(name);
1520
+ table = emptyTable(name, false);
1521
+ state.tables.set(name, table);
1522
+ }
1523
+ else if (table === undefined) {
1524
+ table = emptyTable(name, true);
1525
+ state.tables.set(name, table);
1526
+ }
1527
+ mctx.touched.push(name);
1528
+ runBlueprint(argumentAt(call.args, 1, 'callback'), table, mctx);
1529
+ return;
1530
+ }
1531
+ case 'drop':
1532
+ case 'dropIfExists': {
1533
+ const name = tableNameOf();
1534
+ if (name !== undefined) {
1535
+ mctx.touched.push(name);
1536
+ state.tables.delete(name);
1537
+ }
1538
+ return;
1539
+ }
1540
+ case 'rename': {
1541
+ const from = stringArgument(call.args, 0, 'from');
1542
+ const to = stringArgument(call.args, 1, 'to');
1543
+ if (typeof from !== 'string' || typeof to !== 'string') {
1544
+ pushWarning(state.warnings, `${mctx.file}: Schema::rename() was given table names that are not string literals; the call was skipped.`);
1545
+ return;
1546
+ }
1547
+ const table = state.tables.get(from);
1548
+ mctx.touched.push(from, to);
1549
+ if (table === undefined) {
1550
+ return;
1551
+ }
1552
+ const rebuilt = new Map();
1553
+ for (const [name, existing] of state.tables) {
1554
+ if (name === from) {
1555
+ rebuilt.set(to, { ...existing, name: to });
1556
+ }
1557
+ else {
1558
+ rebuilt.set(name, existing);
1559
+ }
1560
+ }
1561
+ state.tables = rebuilt;
1562
+ for (const other of state.tables.values()) {
1563
+ for (const key of other.foreignKeys) {
1564
+ if (key.refTable === from) {
1565
+ key.refTable = to;
1566
+ }
1567
+ }
1568
+ }
1569
+ return;
1570
+ }
1571
+ case 'dropColumns': {
1572
+ const name = tableNameOf();
1573
+ const columns = stringListArgument(argumentAt(call.args, 1, 'columns'));
1574
+ const table = name === undefined ? undefined : state.tables.get(name);
1575
+ if (columns === null || columns === undefined) {
1576
+ pushWarning(state.warnings, `${mctx.file}: Schema::dropColumns() was given columns that are not string literals; the call was skipped.`);
1577
+ }
1578
+ else if (table !== undefined) {
1579
+ mctx.touched.push(table.name);
1580
+ for (const column of columns) {
1581
+ removeColumn(table, column);
1582
+ }
1583
+ }
1584
+ return;
1585
+ }
1586
+ default:
1587
+ if (!IGNORED_SCHEMA_METHODS.has(method) &&
1588
+ !method.startsWith('has') &&
1589
+ !method.startsWith('get')) {
1590
+ pushWarning(state.warnings, `${mctx.file}: Schema::${method}() is not interpreted and was ignored.`);
1591
+ }
1592
+ return;
1593
+ }
1594
+ }
1595
+ function replayMigration(migration, state) {
1596
+ const mctx = {
1597
+ state,
1598
+ file: migration.file,
1599
+ touched: [],
1600
+ };
1601
+ const statements = migration.up.namedChildren;
1602
+ forEachStatement(statements, (statement) => {
1603
+ const expression = statementExpression(statement);
1604
+ if (expression === undefined) {
1605
+ return;
1606
+ }
1607
+ const chain = readChain(expression);
1608
+ if (chain === undefined) {
1609
+ return;
1610
+ }
1611
+ if (isFacade(chain, 'Schema')) {
1612
+ runSchemaCall(chain, mctx);
1613
+ }
1614
+ else if (isFacade(chain, 'DB')) {
1615
+ warnRawSql(chain, mctx, statement.text);
1616
+ }
1617
+ }, (statement, descend) => describeControlFlow(statement, descend, mctx));
1618
+ }
1619
+ function declarationBody(node) {
1620
+ const body = node.childForFieldName('body');
1621
+ if (body !== null) {
1622
+ return body;
1623
+ }
1624
+ return node.namedChildren.find((child) => child.type === 'declaration_list');
1625
+ }
1626
+ function methodName(method) {
1627
+ return method.childForFieldName('name')?.text ?? '';
1628
+ }
1629
+ /** A class is a migration when its `up()` method calls `Schema::`. */
1630
+ function readMigration(node, ctx) {
1631
+ const body = declarationBody(node);
1632
+ if (body === undefined) {
1633
+ return undefined;
1634
+ }
1635
+ const up = body.namedChildren.find((member) => member.type === 'method_declaration' && methodName(member) === 'up');
1636
+ const upBody = up?.childForFieldName('body') ?? null;
1637
+ if (upBody === null || !/\bSchema\s*::/.test(upBody.text)) {
1638
+ return undefined;
1639
+ }
1640
+ return { file: basename(ctx.path), ctx, up: upBody };
1641
+ }
1642
+ function propertyValue(element) {
1643
+ const initializer = element.namedChildren.find((child) => child.type === 'property_initializer');
1644
+ return initializer?.namedChildren[0];
1645
+ }
1646
+ function castsFromArray(node) {
1647
+ const casts = new Map();
1648
+ if (node === undefined) {
1649
+ return casts;
1650
+ }
1651
+ const value = evaluateNode(node);
1652
+ if (value.kind !== 'array') {
1653
+ return casts;
1654
+ }
1655
+ for (const item of value.items) {
1656
+ if (item.key !== undefined && item.key.kind === 'string') {
1657
+ casts.set(item.key.value, item.value);
1658
+ }
1659
+ }
1660
+ return casts;
1661
+ }
1662
+ function relationFromMethod(method, owner, warnings) {
1663
+ const name = methodName(method);
1664
+ const body = method.childForFieldName('body');
1665
+ const returnType = method.namedChildren.find((child) => child.type === 'named_type' ||
1666
+ child.type === 'optional_type' ||
1667
+ child.type === 'union_type');
1668
+ const declaresRelation = returnType !== undefined &&
1669
+ RELATION_RETURN_TYPES.has(lastNameSegment(returnType.text.replace(/^\?/, '')));
1670
+ if (body === null) {
1671
+ return undefined;
1672
+ }
1673
+ const returned = body.namedChildren.find((child) => child.type === 'return_statement');
1674
+ const expression = returned?.namedChildren[0];
1675
+ const chain = expression === undefined ? undefined : readChain(expression);
1676
+ const first = chain?.links[0];
1677
+ if (chain === undefined ||
1678
+ first === undefined ||
1679
+ chain.isStatic ||
1680
+ chain.root.text !== '$this' ||
1681
+ !RELATION_METHODS.has(first.name)) {
1682
+ if (declaresRelation) {
1683
+ warnings.push(`${owner.name}::${name}(): the relationship method could not be analysed (it does not simply return a $this->hasMany()-style call); the relation was skipped.`);
1684
+ }
1685
+ return undefined;
1686
+ }
1687
+ const extraModifiers = chain.links
1688
+ .slice(1)
1689
+ .map((link) => link.name)
1690
+ .filter((modifier) => !HARMLESS_RELATION_MODIFIERS.has(modifier));
1691
+ const call = first.name;
1692
+ const optionalString = (index, parameter) => {
1693
+ const text = stringArgument(first.args, index, parameter);
1694
+ if (text === null) {
1695
+ warnings.push(`${owner.name}::${name}(): the ${parameter} argument of ${call}() is not a string literal and was ignored.`);
1696
+ return undefined;
1697
+ }
1698
+ return text;
1699
+ };
1700
+ const relatedArgument = argumentAt(first.args, 0, 'related');
1701
+ const relatedFqn = relatedArgument === undefined
1702
+ ? undefined
1703
+ : classReference(evaluateArgument(relatedArgument.node), owner.ctx, owner.fqn);
1704
+ const base = {
1705
+ name,
1706
+ call,
1707
+ kind: 'morph',
1708
+ ...(relatedFqn === undefined ? {} : { relatedFqn }),
1709
+ extraModifiers,
1710
+ };
1711
+ switch (call) {
1712
+ case 'hasOne':
1713
+ case 'hasMany': {
1714
+ const foreignKey = optionalString(1, 'foreignKey');
1715
+ const otherKey = optionalString(2, 'localKey');
1716
+ return {
1717
+ ...base,
1718
+ kind: call,
1719
+ ...(foreignKey === undefined ? {} : { foreignKey }),
1720
+ ...(otherKey === undefined ? {} : { otherKey }),
1721
+ };
1722
+ }
1723
+ case 'belongsTo': {
1724
+ const foreignKey = optionalString(1, 'foreignKey');
1725
+ const otherKey = optionalString(2, 'ownerKey');
1726
+ return {
1727
+ ...base,
1728
+ kind: 'belongsTo',
1729
+ ...(foreignKey === undefined ? {} : { foreignKey }),
1730
+ ...(otherKey === undefined ? {} : { otherKey }),
1731
+ };
1732
+ }
1733
+ case 'belongsToMany': {
1734
+ const tableArgument = argumentAt(first.args, 1, 'table');
1735
+ let pivotTable;
1736
+ let pivotModelFqn;
1737
+ if (tableArgument !== undefined) {
1738
+ const value = evaluateArgument(tableArgument.node);
1739
+ if (value.kind === 'classRef') {
1740
+ pivotModelFqn = classReference(value, owner.ctx, owner.fqn);
1741
+ }
1742
+ else if (value.kind === 'string') {
1743
+ pivotTable = value.value;
1744
+ }
1745
+ else {
1746
+ warnings.push(`${owner.name}::${name}(): the table argument of belongsToMany() is not a string literal and was ignored.`);
1747
+ }
1748
+ }
1749
+ const foreignKey = optionalString(2, 'foreignPivotKey');
1750
+ const relatedPivotKey = optionalString(3, 'relatedPivotKey');
1751
+ const parentKey = optionalString(4, 'parentKey');
1752
+ const relatedKey = optionalString(5, 'relatedKey');
1753
+ return {
1754
+ ...base,
1755
+ kind: 'belongsToMany',
1756
+ ...(pivotTable === undefined ? {} : { pivotTable }),
1757
+ ...(pivotModelFqn === undefined ? {} : { pivotModelFqn }),
1758
+ ...(foreignKey === undefined ? {} : { foreignKey }),
1759
+ ...(relatedPivotKey === undefined ? {} : { relatedPivotKey }),
1760
+ ...(parentKey === undefined ? {} : { parentKey }),
1761
+ ...(relatedKey === undefined ? {} : { relatedKey }),
1762
+ };
1763
+ }
1764
+ case 'hasOneThrough':
1765
+ case 'hasManyThrough':
1766
+ case 'hasOneOfMany':
1767
+ case 'latestOfMany':
1768
+ case 'oldestOfMany':
1769
+ return { ...base, kind: 'through' };
1770
+ default:
1771
+ return base;
1772
+ }
1773
+ }
1774
+ function readModelClass(node, ctx, warnings) {
1775
+ const nameNode = node.childForFieldName('name');
1776
+ const body = declarationBody(node);
1777
+ if (nameNode === null || body === undefined) {
1778
+ return undefined;
1779
+ }
1780
+ const fqn = ctx.namespace === '' ? nameNode.text : `${ctx.namespace}\\${nameNode.text}`;
1781
+ const model = {
1782
+ name: nameNode.text,
1783
+ fqn,
1784
+ ctx,
1785
+ isAbstract: node.namedChildren.some((child) => child.type === 'abstract_modifier'),
1786
+ casts: new Map(),
1787
+ traits: [],
1788
+ relations: [],
1789
+ };
1790
+ const baseClause = node.namedChildren.find((child) => child.type === 'base_clause');
1791
+ const baseName = baseClause?.namedChildren[0];
1792
+ if (baseName !== undefined) {
1793
+ model.parentFqn = resolveClassName(baseName.text.replace(/\s+/g, ''), ctx);
1794
+ }
1795
+ for (const member of body.namedChildren) {
1796
+ if (member.type === 'use_declaration') {
1797
+ for (const used of member.namedChildren) {
1798
+ if (used.type === 'name' || used.type === 'qualified_name') {
1799
+ model.traits.push(resolveClassName(used.text.replace(/\s+/g, ''), ctx));
1800
+ }
1801
+ }
1802
+ }
1803
+ else if (member.type === 'const_declaration') {
1804
+ for (const element of member.namedChildren) {
1805
+ if (element.type !== 'const_element') {
1806
+ continue;
1807
+ }
1808
+ const constName = element.namedChildren[0]?.text;
1809
+ const valueNode = element.namedChildren[1];
1810
+ if (valueNode === undefined) {
1811
+ continue;
1812
+ }
1813
+ const value = evaluateNode(valueNode);
1814
+ const text = value.kind === 'string'
1815
+ ? value.value
1816
+ : value.kind === 'null'
1817
+ ? null
1818
+ : undefined;
1819
+ if (text === undefined) {
1820
+ continue;
1821
+ }
1822
+ if (constName === 'CREATED_AT') {
1823
+ model.createdAt = text;
1824
+ }
1825
+ else if (constName === 'UPDATED_AT') {
1826
+ model.updatedAt = text;
1827
+ }
1828
+ else if (constName === 'DELETED_AT' && text !== null) {
1829
+ model.deletedAt = text;
1830
+ }
1831
+ }
1832
+ }
1833
+ else if (member.type === 'property_declaration') {
1834
+ readModelProperty(member, model, warnings);
1835
+ }
1836
+ else if (member.type === 'method_declaration') {
1837
+ const memberName = methodName(member);
1838
+ const isStatic = member.namedChildren.some((child) => child.type === 'static_modifier');
1839
+ const visibility = member.namedChildren.find((child) => child.type === 'visibility_modifier')?.text;
1840
+ if (memberName === 'casts') {
1841
+ const returned = member
1842
+ .childForFieldName('body')
1843
+ ?.namedChildren.find((child) => child.type === 'return_statement');
1844
+ for (const [key, value] of castsFromArray(returned?.namedChildren[0])) {
1845
+ model.casts.set(key, value);
1846
+ }
1847
+ }
1848
+ else if (memberName === 'uniqueIds') {
1849
+ const returned = member
1850
+ .childForFieldName('body')
1851
+ ?.namedChildren.find((child) => child.type === 'return_statement');
1852
+ const list = returned?.namedChildren[0] === undefined
1853
+ ? undefined
1854
+ : evaluateNode(returned.namedChildren[0]);
1855
+ if (list !== undefined && list.kind === 'array') {
1856
+ model.uniqueIds = list.items.flatMap((item) => item.value.kind === 'string' ? [item.value.value] : []);
1857
+ }
1858
+ }
1859
+ else if (!isStatic &&
1860
+ (visibility === undefined || visibility === 'public')) {
1861
+ const relation = relationFromMethod(member, model, warnings);
1862
+ if (relation !== undefined) {
1863
+ model.relations.push(relation);
1864
+ }
1865
+ }
1866
+ }
1867
+ }
1868
+ return model;
1869
+ }
1870
+ function readModelProperty(declaration, model, warnings) {
1871
+ if (declaration.namedChildren.some((child) => child.type === 'static_modifier')) {
1872
+ return;
1873
+ }
1874
+ for (const element of declaration.namedChildren) {
1875
+ if (element.type !== 'property_element') {
1876
+ continue;
1877
+ }
1878
+ const name = element.namedChildren[0]?.text.replace(/^\$/, '');
1879
+ const valueNode = propertyValue(element);
1880
+ if (name === undefined || valueNode === undefined) {
1881
+ continue;
1882
+ }
1883
+ if (name === 'casts') {
1884
+ for (const [key, value] of castsFromArray(valueNode)) {
1885
+ model.casts.set(key, value);
1886
+ }
1887
+ continue;
1888
+ }
1889
+ const value = evaluateNode(valueNode);
1890
+ switch (name) {
1891
+ case 'table':
1892
+ case 'primaryKey':
1893
+ case 'keyType':
1894
+ if (value.kind === 'string') {
1895
+ if (name === 'table') {
1896
+ model.table = value.value;
1897
+ }
1898
+ else if (name === 'primaryKey') {
1899
+ model.primaryKey = value.value;
1900
+ }
1901
+ else {
1902
+ model.keyType = value.value;
1903
+ }
1904
+ }
1905
+ else {
1906
+ warnings.push(`${model.name}: $${name} is not a string literal and was ignored.`);
1907
+ }
1908
+ break;
1909
+ case 'incrementing':
1910
+ case 'timestamps':
1911
+ if (value.kind === 'bool') {
1912
+ if (name === 'timestamps') {
1913
+ model.timestamps = value.value;
1914
+ }
1915
+ else {
1916
+ model.incrementing = value.value;
1917
+ }
1918
+ }
1919
+ break;
1920
+ default:
1921
+ break;
1922
+ }
1923
+ }
1924
+ }
1925
+ function discover(sources, parser, warnings) {
1926
+ const found = { migrations: [], models: [], enums: new Map() };
1927
+ const classNodes = [];
1928
+ for (const source of sources) {
1929
+ const tree = parser.parse(source.text);
1930
+ if (tree.rootNode.hasError) {
1931
+ warnings.push(`${source.path}: the file contains PHP syntax errors; some migrations, models or columns may be missing from the output.`);
1932
+ }
1933
+ for (const { ctx, node } of collectDeclarations(tree.rootNode, source.path, true)) {
1934
+ if (node.type === 'enum_declaration') {
1935
+ const info = readEnumDeclaration(node, ctx, warnings);
1936
+ if (info !== undefined && !found.enums.has(info.fqn)) {
1937
+ found.enums.set(info.fqn, info);
1938
+ }
1939
+ continue;
1940
+ }
1941
+ const migration = readMigration(node, ctx);
1942
+ if (migration !== undefined) {
1943
+ found.migrations.push({
1944
+ key: basename(source.path).replace(/\.php$/i, ''),
1945
+ migration,
1946
+ });
1947
+ }
1948
+ else if (node.type === 'class_declaration') {
1949
+ classNodes.push({ node, ctx });
1950
+ }
1951
+ }
1952
+ }
1953
+ // A class is a model when it extends an Eloquent base class, directly or through another model.
1954
+ const candidates = [];
1955
+ for (const { node, ctx } of classNodes) {
1956
+ const model = readModelClass(node, ctx, warnings);
1957
+ if (model !== undefined && model.parentFqn !== undefined) {
1958
+ candidates.push(model);
1959
+ }
1960
+ }
1961
+ const projectClasses = new Set(candidates.map((model) => model.fqn));
1962
+ const isBase = (model) => {
1963
+ const parent = model.parentFqn;
1964
+ if (parent === undefined) {
1965
+ return false;
1966
+ }
1967
+ if (MODEL_BASE_CLASSES.has(parent)) {
1968
+ return true;
1969
+ }
1970
+ // A short name that did not resolve to a class of the project (the import is missing, or the file is read on its own).
1971
+ return (MODEL_BASE_SHORT_NAMES.has(lastNameSegment(parent)) &&
1972
+ !projectClasses.has(parent));
1973
+ };
1974
+ const accepted = new Map();
1975
+ let changed = true;
1976
+ while (changed) {
1977
+ changed = false;
1978
+ for (const model of candidates) {
1979
+ if (accepted.has(model.fqn)) {
1980
+ continue;
1981
+ }
1982
+ if (isBase(model) ||
1983
+ (model.parentFqn !== undefined && accepted.has(model.parentFqn))) {
1984
+ accepted.set(model.fqn, model);
1985
+ changed = true;
1986
+ }
1987
+ }
1988
+ }
1989
+ found.models = candidates.filter((model) => accepted.has(model.fqn));
1990
+ return found;
1991
+ }
1992
+ // ---------------------------------------------------------------------------
1993
+ // Models resolved through their parents
1994
+ // ---------------------------------------------------------------------------
1995
+ function resolveModel(model, all) {
1996
+ const chain = [];
1997
+ const seen = new Set();
1998
+ let current = model;
1999
+ while (current !== undefined && !seen.has(current.fqn)) {
2000
+ chain.push(current);
2001
+ seen.add(current.fqn);
2002
+ current =
2003
+ current.parentFqn === undefined ? undefined : all.get(current.parentFqn);
2004
+ }
2005
+ const firstDefined = (read) => {
2006
+ for (const cls of chain) {
2007
+ const value = read(cls);
2008
+ if (value !== undefined) {
2009
+ return value;
2010
+ }
2011
+ }
2012
+ return undefined;
2013
+ };
2014
+ const casts = new Map();
2015
+ for (const cls of [...chain].reverse()) {
2016
+ for (const [key, value] of cls.casts) {
2017
+ casts.set(key, value);
2018
+ }
2019
+ }
2020
+ const traits = chain.flatMap((cls) => cls.traits);
2021
+ const hasTrait = (...names) => traits.some((trait) => names.includes(lastNameSegment(trait)));
2022
+ const timestamps = firstDefined((cls) => cls.timestamps);
2023
+ const createdAt = firstDefined((cls) => cls.createdAt);
2024
+ const updatedAt = firstDefined((cls) => cls.updatedAt);
2025
+ const primaryKey = firstDefined((cls) => cls.primaryKey);
2026
+ const keyType = firstDefined((cls) => cls.keyType);
2027
+ const incrementing = firstDefined((cls) => cls.incrementing);
2028
+ const uniqueIds = firstDefined((cls) => cls.uniqueIds);
2029
+ return {
2030
+ source: model,
2031
+ table: model.table ?? conventionalTableName(model.name),
2032
+ ...(primaryKey === undefined ? {} : { primaryKey }),
2033
+ ...(keyType === undefined ? {} : { keyType }),
2034
+ ...(incrementing === undefined ? {} : { incrementing }),
2035
+ timestamps: timestamps ?? true,
2036
+ createdAt: createdAt === undefined ? 'created_at' : createdAt,
2037
+ updatedAt: updatedAt === undefined ? 'updated_at' : updatedAt,
2038
+ deletedAt: firstDefined((cls) => cls.deletedAt) ?? 'deleted_at',
2039
+ ...(uniqueIds === undefined ? {} : { uniqueIds }),
2040
+ casts,
2041
+ usesSoftDeletes: hasTrait('SoftDeletes'),
2042
+ usesUuids: hasTrait('HasUuids', 'HasVersion4Uuids', 'HasVersion7Uuids'),
2043
+ usesUlids: hasTrait('HasUlids'),
2044
+ };
2045
+ }
2046
+ // ---------------------------------------------------------------------------
2047
+ // Public entry point
2048
+ // ---------------------------------------------------------------------------
2049
+ /**
2050
+ * Parses a Laravel project into the shared IR. The schema is rebuilt by replaying
2051
+ * the migrations in file-name order (the source of truth for columns, indexes and
2052
+ * foreign keys); Eloquent models contribute relation names, casts to backed enums,
2053
+ * custom table names and timestamps handling.
2054
+ */
2055
+ export async function parseLaravel(sources, options) {
2056
+ const parserResult = await getPhpParser();
2057
+ if (!parserResult.ok) {
2058
+ return parserResult;
2059
+ }
2060
+ const warnings = [];
2061
+ const found = discover(sources, parserResult.value, warnings);
2062
+ if (found.migrations.length === 0) {
2063
+ const checkedPaths = sources
2064
+ .map((source) => source.path)
2065
+ .join(', ');
2066
+ return err('NO_MODELS_FOUND', `No Laravel migrations were found in: ${checkedPaths}. A migration is a class whose up() method calls Schema::create() or Schema::table(); ` +
2067
+ `Laravel keeps its schema in migrations, so pass the database/migrations directory (or the project root) together with the models.`);
2068
+ }
2069
+ const ordered = [...found.migrations].sort((first, second) => first.key < second.key ? -1 : first.key > second.key ? 1 : 0);
2070
+ const hasPrefix = (key) => /^\d{4}_\d{2}_\d{2}_\d{6}_/.test(key);
2071
+ if (ordered.length > 1) {
2072
+ for (const entry of ordered) {
2073
+ if (!hasPrefix(entry.key)) {
2074
+ warnings.push(`${entry.migration.file}: the migration file name has no timestamp prefix (YYYY_MM_DD_HHMMSS_); migrations were ordered by file name.`);
2075
+ }
2076
+ }
2077
+ }
2078
+ const state = { tables: new Map(), warnings };
2079
+ for (const entry of ordered) {
2080
+ replayMigration(entry.migration, state);
2081
+ }
2082
+ const schema = buildSchema(state, found, options);
2083
+ if (schema.models.length === 0) {
2084
+ return err('NO_MODELS_FOUND', `The Laravel migrations in ${sources.map((source) => source.path).join(', ')} leave no tables behind ` +
2085
+ `(every table is dropped again, or only altered without being created).`);
2086
+ }
2087
+ return ok(schema);
2088
+ }
2089
+ function singlePrimaryKey(table) {
2090
+ return table?.primaryKey?.length === 1 ? table.primaryKey[0] : undefined;
2091
+ }
2092
+ function buildSchema(state, found, options) {
2093
+ const warnings = state.warnings;
2094
+ // Tables -----------------------------------------------------------------
2095
+ const live = new Map();
2096
+ for (const table of state.tables.values()) {
2097
+ if (table.isPartial) {
2098
+ warnings.push(`${table.name}: the table is altered by the migrations but never created in the provided files; it was skipped.`);
2099
+ }
2100
+ else {
2101
+ live.set(table.name, table);
2102
+ }
2103
+ }
2104
+ // Models -----------------------------------------------------------------
2105
+ const classesByFqn = new Map(found.models.map((model) => [
2106
+ model.fqn,
2107
+ model,
2108
+ ]));
2109
+ const effectiveByFqn = new Map();
2110
+ const effectiveByTable = new Map();
2111
+ for (const model of found.models) {
2112
+ if (model.isAbstract) {
2113
+ continue;
2114
+ }
2115
+ const effective = resolveModel(model, classesByFqn);
2116
+ if (!live.has(effective.table)) {
2117
+ warnings.push(`${model.name}: the model maps to the table "${effective.table}", which no migration creates; the model was skipped.`);
2118
+ continue;
2119
+ }
2120
+ const taken = effectiveByTable.get(effective.table);
2121
+ if (taken !== undefined) {
2122
+ warnings.push(`${model.name}: the table "${effective.table}" is already used by the model ${taken.source.name}; ${model.name} was ignored.`);
2123
+ continue;
2124
+ }
2125
+ effectiveByTable.set(effective.table, effective);
2126
+ effectiveByFqn.set(model.fqn, effective);
2127
+ }
2128
+ const keyName = (effective) => effective.primaryKey ?? singlePrimaryKey(live.get(effective.table)) ?? 'id';
2129
+ // Pivot tables -------------------------------------------------------------
2130
+ const uses = [];
2131
+ for (const effective of effectiveByTable.values()) {
2132
+ const owner = effective.source;
2133
+ for (const method of owner.relations) {
2134
+ if (method.kind !== 'belongsToMany') {
2135
+ continue;
2136
+ }
2137
+ const related = method.relatedFqn === undefined
2138
+ ? undefined
2139
+ : effectiveByFqn.get(method.relatedFqn);
2140
+ if (related === undefined) {
2141
+ warnings.push(`${owner.name}::${method.name}(): the related class is not a model with a table in the provided files; the relation was skipped.`);
2142
+ continue;
2143
+ }
2144
+ const pivotModel = method.pivotModelFqn === undefined
2145
+ ? undefined
2146
+ : effectiveByFqn.get(method.pivotModelFqn);
2147
+ const table = method.pivotTable ??
2148
+ pivotModel?.table ??
2149
+ [eloquentSnake(owner.name), eloquentSnake(related.source.name)]
2150
+ .sort()
2151
+ .join('_');
2152
+ const ownerColumn = method.foreignKey ??
2153
+ `${eloquentSnake(owner.name)}_${keyName(effective)}`;
2154
+ const relatedColumn = method.relatedPivotKey ??
2155
+ `${eloquentSnake(related.source.name)}_${keyName(related)}`;
2156
+ const pivot = live.get(table);
2157
+ if (pivot === undefined) {
2158
+ warnings.push(`${owner.name}::${method.name}(): the pivot table "${table}" is not created by the migrations; the relation was skipped.`);
2159
+ continue;
2160
+ }
2161
+ if (!pivot.columns.has(ownerColumn) ||
2162
+ !pivot.columns.has(relatedColumn)) {
2163
+ warnings.push(`${owner.name}::${method.name}(): the pivot table "${table}" has no column "${pivot.columns.has(ownerColumn) ? relatedColumn : ownerColumn}"; the relation was skipped.`);
2164
+ continue;
2165
+ }
2166
+ if (method.parentKey !== undefined || method.relatedKey !== undefined) {
2167
+ warnings.push(`${owner.name}::${method.name}(): the parentKey / relatedKey arguments (keys other than the primary key) are not represented.`);
2168
+ }
2169
+ const extraColumns = [...pivot.columns.keys()].filter((name) => name !== ownerColumn &&
2170
+ name !== relatedColumn &&
2171
+ !(name === 'id' && pivot.primaryKey?.join() === 'id') &&
2172
+ name !== 'created_at' &&
2173
+ name !== 'updated_at');
2174
+ const isFoldable = extraColumns.length === 0;
2175
+ if (!isFoldable) {
2176
+ warnings.push(`${owner.name}::${method.name}(): the pivot table "${table}" has extra columns (${extraColumns.join(', ')}), so it was kept as a model and the relation was not converted to a many-to-many field.`);
2177
+ }
2178
+ uses.push({
2179
+ owner: effective,
2180
+ related,
2181
+ method,
2182
+ table,
2183
+ ownerColumn,
2184
+ relatedColumn,
2185
+ isFoldable,
2186
+ });
2187
+ }
2188
+ }
2189
+ const folded = new Set();
2190
+ for (const table of unique(uses.map((use) => use.table))) {
2191
+ if (uses
2192
+ .filter((use) => use.table === table)
2193
+ .every((use) => use.isFoldable)) {
2194
+ folded.add(table);
2195
+ }
2196
+ }
2197
+ // Names ------------------------------------------------------------------
2198
+ const modelNames = new Map();
2199
+ const takenNames = new Set();
2200
+ for (const table of live.keys()) {
2201
+ const preferred = effectiveByTable.get(table)?.source.name ??
2202
+ toPascalCase(singularize(table));
2203
+ let name = preferred === '' ? 'Model' : preferred;
2204
+ while (takenNames.has(name)) {
2205
+ name = `${name}Table`;
2206
+ }
2207
+ takenNames.add(name);
2208
+ modelNames.set(table, name);
2209
+ }
2210
+ const tableForClassName = (className) => {
2211
+ const short = lastNameSegment(className);
2212
+ for (const [table, effective] of effectiveByTable) {
2213
+ if (effective.source.name === short) {
2214
+ return table;
2215
+ }
2216
+ }
2217
+ return conventionalTableName(short);
2218
+ };
2219
+ // Foreign key candidates -------------------------------------------------
2220
+ const candidates = new Map();
2221
+ const candidatesOf = (table) => {
2222
+ let map = candidates.get(table);
2223
+ if (map === undefined) {
2224
+ map = new Map();
2225
+ candidates.set(table, map);
2226
+ }
2227
+ return map;
2228
+ };
2229
+ const isTarget = (table) => live.has(table) && !folded.has(table);
2230
+ for (const table of live.values()) {
2231
+ if (folded.has(table.name)) {
2232
+ continue;
2233
+ }
2234
+ for (const key of table.foreignKeys) {
2235
+ const column = key.columns[0];
2236
+ if (key.columns.length !== 1 || column === undefined) {
2237
+ warnings.push(`${table.name}: the composite foreign key (${key.columns.join(', ')}) is not supported; its columns were kept as plain columns.`);
2238
+ continue;
2239
+ }
2240
+ if (!table.columns.has(column)) {
2241
+ continue;
2242
+ }
2243
+ const referenced = key.refTable ??
2244
+ (key.refModel === undefined
2245
+ ? undefined
2246
+ : tableForClassName(key.refModel));
2247
+ if (referenced === undefined) {
2248
+ warnings.push(`${table.name}.${column}: the foreign key does not name a referenced table; the column was kept as a plain column.`);
2249
+ continue;
2250
+ }
2251
+ if (!isTarget(referenced)) {
2252
+ warnings.push(`${table.name}.${column}: the foreign key references the table "${referenced}", which no migration creates; the column was kept as a plain column.`);
2253
+ continue;
2254
+ }
2255
+ candidatesOf(table.name).set(column, {
2256
+ column,
2257
+ targetTable: referenced,
2258
+ targetColumn: key.refColumns[0] ?? 'id',
2259
+ foreignKey: key,
2260
+ });
2261
+ }
2262
+ }
2263
+ for (const effective of effectiveByTable.values()) {
2264
+ const owner = effective.source;
2265
+ for (const method of owner.relations) {
2266
+ if (method.kind === 'morph' || method.kind === 'through') {
2267
+ warnings.push(method.kind === 'morph'
2268
+ ? `${owner.name}::${method.name}(): the polymorphic relation (${method.call}) is not represented; its columns, if any, were kept as plain columns.`
2269
+ : `${owner.name}::${method.name}(): the ${method.call} relation is derived from other relations and was not converted.`);
2270
+ continue;
2271
+ }
2272
+ if (method.extraModifiers.length > 0) {
2273
+ warnings.push(`${owner.name}::${method.name}(): the relation is further constrained by ${method.extraModifiers.join(', ')}(); the constraint is not represented.`);
2274
+ }
2275
+ if (method.kind === 'belongsToMany') {
2276
+ continue;
2277
+ }
2278
+ const related = method.relatedFqn === undefined
2279
+ ? undefined
2280
+ : effectiveByFqn.get(method.relatedFqn);
2281
+ if (related === undefined) {
2282
+ warnings.push(`${owner.name}::${method.name}(): the related class is not a model with a table in the provided files; the relation was skipped.`);
2283
+ continue;
2284
+ }
2285
+ if (method.kind === 'belongsTo') {
2286
+ const ownerKey = method.otherKey ?? keyName(related);
2287
+ const column = method.foreignKey ?? `${eloquentSnake(method.name)}_${ownerKey}`;
2288
+ const table = live.get(effective.table);
2289
+ if (table === undefined || !table.columns.has(column)) {
2290
+ warnings.push(`${owner.name}::${method.name}(): belongsTo uses the column "${column}", which the table "${effective.table}" does not have; the relation was skipped.`);
2291
+ continue;
2292
+ }
2293
+ if (!isTarget(related.table)) {
2294
+ continue;
2295
+ }
2296
+ const existing = candidatesOf(effective.table).get(column);
2297
+ if (existing === undefined) {
2298
+ candidatesOf(effective.table).set(column, {
2299
+ column,
2300
+ targetTable: related.table,
2301
+ targetColumn: ownerKey,
2302
+ name: method.name,
2303
+ });
2304
+ }
2305
+ else {
2306
+ existing.name = method.name;
2307
+ if (existing.targetTable !== related.table) {
2308
+ warnings.push(`${owner.name}::${method.name}(): belongsTo points at "${related.table}" but the foreign key on ${effective.table}.${column} references "${existing.targetTable}"; the foreign key was used.`);
2309
+ }
2310
+ }
2311
+ continue;
2312
+ }
2313
+ // hasOne / hasMany: the foreign key lives on the related table.
2314
+ const column = method.foreignKey ??
2315
+ `${eloquentSnake(owner.name)}_${keyName(effective)}`;
2316
+ const localKey = method.otherKey ?? keyName(effective);
2317
+ const relatedTable = live.get(related.table);
2318
+ if (relatedTable === undefined || !isTarget(related.table)) {
2319
+ continue;
2320
+ }
2321
+ if (!relatedTable.columns.has(column)) {
2322
+ warnings.push(`${owner.name}::${method.name}(): ${method.call} uses the column "${column}", which the table "${related.table}" does not have; the relation was skipped.`);
2323
+ continue;
2324
+ }
2325
+ const existing = candidatesOf(related.table).get(column);
2326
+ const reverse = {
2327
+ name: method.name,
2328
+ many: method.kind === 'hasMany',
2329
+ };
2330
+ if (existing === undefined) {
2331
+ candidatesOf(related.table).set(column, {
2332
+ column,
2333
+ targetTable: effective.table,
2334
+ targetColumn: localKey,
2335
+ reverse,
2336
+ });
2337
+ }
2338
+ else if (existing.targetTable !== effective.table) {
2339
+ warnings.push(`${owner.name}::${method.name}(): ${method.call} expects ${related.table}.${column} to reference "${effective.table}" but it references "${existing.targetTable}"; the reverse name was not applied.`);
2340
+ }
2341
+ else if (existing.reverse === undefined) {
2342
+ existing.reverse = reverse;
2343
+ }
2344
+ }
2345
+ }
2346
+ // Pivot tables kept as models still need their relations.
2347
+ for (const use of uses) {
2348
+ if (folded.has(use.table)) {
2349
+ continue;
2350
+ }
2351
+ const pairs = [
2352
+ [use.ownerColumn, use.owner],
2353
+ [use.relatedColumn, use.related],
2354
+ ];
2355
+ for (const [column, target] of pairs) {
2356
+ if (!candidatesOf(use.table).has(column) && isTarget(target.table)) {
2357
+ candidatesOf(use.table).set(column, {
2358
+ column,
2359
+ targetTable: target.table,
2360
+ targetColumn: keyName(target),
2361
+ });
2362
+ }
2363
+ }
2364
+ }
2365
+ // Enums ------------------------------------------------------------------
2366
+ const enums = new Map();
2367
+ const findEnum = (className, model) => {
2368
+ const resolved = resolveClassName(className, model.ctx);
2369
+ const exact = found.enums.get(resolved);
2370
+ if (exact !== undefined) {
2371
+ return exact;
2372
+ }
2373
+ const short = [...found.enums.values()].filter((info) => info.name === lastNameSegment(className));
2374
+ return short.length === 1 ? short[0] : undefined;
2375
+ };
2376
+ const registerEnum = (info) => {
2377
+ if (!enums.has(info.name)) {
2378
+ enums.set(info.name, { name: info.name, values: info.values });
2379
+ }
2380
+ };
2381
+ const synthesizeEnum = (modelName, column) => {
2382
+ const values = [];
2383
+ const seen = new Set();
2384
+ (column.enumValues ?? []).forEach((value, position) => {
2385
+ let name = toPascalCase(value);
2386
+ if (name === '' || /^\d/.test(name)) {
2387
+ name = `Value${position + 1}`;
2388
+ }
2389
+ while (seen.has(name)) {
2390
+ name = `${name}_`;
2391
+ }
2392
+ seen.add(name);
2393
+ values.push({ name, dbValue: value });
2394
+ });
2395
+ let enumName = `${modelName}${toPascalCase(column.name)}`;
2396
+ while (enums.has(enumName)) {
2397
+ enumName = `${enumName}_`;
2398
+ }
2399
+ enums.set(enumName, { name: enumName, values });
2400
+ return enumName;
2401
+ };
2402
+ // Models -----------------------------------------------------------------
2403
+ const models = [];
2404
+ for (const table of live.values()) {
2405
+ if (folded.has(table.name)) {
2406
+ continue;
2407
+ }
2408
+ models.push(buildModel({
2409
+ table,
2410
+ modelName: modelNames.get(table.name) ?? table.name,
2411
+ effective: effectiveByTable.get(table.name),
2412
+ candidates: candidates.get(table.name) ?? new Map(),
2413
+ modelNames,
2414
+ live,
2415
+ enums,
2416
+ findEnum,
2417
+ registerEnum,
2418
+ synthesizeEnum,
2419
+ warnings,
2420
+ appLabel: options.appLabel,
2421
+ }));
2422
+ }
2423
+ addManyToMany(models, uses, folded, modelNames, live, warnings);
2424
+ const referenced = new Set();
2425
+ for (const model of models) {
2426
+ for (const field of model.fields) {
2427
+ if (field.enumName !== undefined) {
2428
+ referenced.add(field.enumName);
2429
+ }
2430
+ }
2431
+ }
2432
+ return {
2433
+ models,
2434
+ enums: [...enums.values()].filter((entry) => referenced.has(entry.name)),
2435
+ warnings,
2436
+ };
2437
+ }
2438
+ function buildModel(input) {
2439
+ const { table, effective, warnings } = input;
2440
+ const primaryKey = table.primaryKey ?? [];
2441
+ const relationColumns = new Set(input.candidates.keys());
2442
+ const uniqueSingles = new Set();
2443
+ for (const index of table.indexes) {
2444
+ const only = index.columns[0];
2445
+ if (index.isUnique &&
2446
+ index.kind === undefined &&
2447
+ index.columns.length === 1 &&
2448
+ only !== undefined) {
2449
+ uniqueSingles.add(only);
2450
+ }
2451
+ }
2452
+ if (primaryKey.length === 1 && primaryKey[0] !== undefined) {
2453
+ uniqueSingles.add(primaryKey[0]);
2454
+ }
2455
+ // Fields -----------------------------------------------------------------
2456
+ const fields = [];
2457
+ for (const column of table.columns.values()) {
2458
+ if (relationColumns.has(column.name)) {
2459
+ continue;
2460
+ }
2461
+ fields.push(buildField(column, table, effective, input));
2462
+ }
2463
+ const usedNames = new Set(fields.map((field) => field.name));
2464
+ // Relations --------------------------------------------------------------
2465
+ const relationNames = new Map();
2466
+ const relations = [];
2467
+ for (const column of table.columns.values()) {
2468
+ const candidate = input.candidates.get(column.name);
2469
+ if (candidate === undefined) {
2470
+ continue;
2471
+ }
2472
+ let name = candidate.name ?? column.name.replace(/_id$/, '');
2473
+ if (name === '') {
2474
+ name = column.name;
2475
+ }
2476
+ while (usedNames.has(name)) {
2477
+ name = `${name}_rel`;
2478
+ }
2479
+ usedNames.add(name);
2480
+ relationNames.set(column.name, name);
2481
+ const isPrimary = primaryKey.length === 1 && primaryKey[0] === column.name;
2482
+ const targetTable = input.live.get(candidate.targetTable);
2483
+ const targetKey = singlePrimaryKey(targetTable) ?? 'id';
2484
+ const kind = candidate.reverse?.many === false ||
2485
+ (candidate.reverse === undefined && uniqueSingles.has(column.name))
2486
+ ? 'oneToOne'
2487
+ : 'foreignKey';
2488
+ const relation = {
2489
+ name,
2490
+ kind,
2491
+ targetModel: input.modelNames.get(candidate.targetTable) ?? candidate.targetTable,
2492
+ columnName: column.name,
2493
+ isNullable: column.isNullable && !isPrimary,
2494
+ onDelete: candidate.foreignKey?.onDelete ?? 'noAction',
2495
+ ...(candidate.reverse === undefined
2496
+ ? {}
2497
+ : { relatedName: candidate.reverse.name }),
2498
+ ...(candidate.targetColumn === targetKey
2499
+ ? {}
2500
+ : { toField: candidate.targetColumn }),
2501
+ ...(isPrimary ? { isPrimaryKey: true } : {}),
2502
+ ...(candidate.foreignKey?.onUpdate === undefined
2503
+ ? {}
2504
+ : { onUpdate: candidate.foreignKey.onUpdate }),
2505
+ };
2506
+ relations.push(relation);
2507
+ }
2508
+ const irName = (column) => relationNames.get(column) ?? column;
2509
+ // Primary key ------------------------------------------------------------
2510
+ let compositePrimaryKey;
2511
+ if (primaryKey.length > 1) {
2512
+ compositePrimaryKey = primaryKey.map(irName);
2513
+ }
2514
+ else if (primaryKey.length === 0) {
2515
+ warnings.push(`${table.name}: the table has no primary key.`);
2516
+ }
2517
+ // Indexes ----------------------------------------------------------------
2518
+ const indexes = [];
2519
+ for (const index of table.indexes) {
2520
+ const missing = index.columns.filter((name) => !table.columns.has(name));
2521
+ if (missing.length > 0) {
2522
+ warnings.push(`${table.name}: the ${index.isUnique ? 'unique constraint' : 'index'} on (${index.columns.join(', ')}) refers to "${missing.join('", "')}", which is not a column of the table; it was skipped.`);
2523
+ continue;
2524
+ }
2525
+ const type = index.kind === 'fulltext'
2526
+ ? 'fulltext'
2527
+ : index.isUnique
2528
+ ? 'unique'
2529
+ : 'index';
2530
+ const conventional = conventionalIndexName(table.name, index.columns, type);
2531
+ const explicitName = index.name !== undefined && index.name !== conventional
2532
+ ? index.name
2533
+ : undefined;
2534
+ const names = index.columns.map(irName);
2535
+ const only = index.columns[0];
2536
+ if (index.isUnique &&
2537
+ index.kind === undefined &&
2538
+ explicitName === undefined &&
2539
+ index.columns.length === 1 &&
2540
+ only !== undefined) {
2541
+ const field = fields.find((candidate) => candidate.columnName === only);
2542
+ if (field !== undefined) {
2543
+ if (!field.isPrimaryKey) {
2544
+ field.isUnique = true;
2545
+ }
2546
+ continue;
2547
+ }
2548
+ if (relationNames.has(only)) {
2549
+ // The relation is one-to-one already; nothing else to record.
2550
+ continue;
2551
+ }
2552
+ }
2553
+ indexes.push({
2554
+ fields: names,
2555
+ isUnique: index.isUnique,
2556
+ ...(explicitName === undefined ? {} : { name: explicitName }),
2557
+ ...(index.kind === undefined ? {} : { kind: index.kind }),
2558
+ });
2559
+ }
2560
+ // Soft deletes -----------------------------------------------------------
2561
+ if (effective !== undefined &&
2562
+ effective.usesSoftDeletes &&
2563
+ !table.columns.has(effective.deletedAt)) {
2564
+ warnings.push(`${effective.source.name}: the model uses SoftDeletes but the table "${table.name}" has no "${effective.deletedAt}" column.`);
2565
+ }
2566
+ return {
2567
+ name: input.modelName,
2568
+ tableName: table.name,
2569
+ appLabel: input.appLabel,
2570
+ fields,
2571
+ relations,
2572
+ indexes,
2573
+ ...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
2574
+ };
2575
+ }
2576
+ function buildField(column, table, effective, input) {
2577
+ const { warnings } = input;
2578
+ const label = `${table.name}.${column.name}`;
2579
+ const isPrimary = table.primaryKey?.length === 1 && table.primaryKey[0] === column.name;
2580
+ const field = {
2581
+ name: column.name,
2582
+ columnName: column.name,
2583
+ type: column.type,
2584
+ isPrimaryKey: isPrimary,
2585
+ isUnique: false,
2586
+ isNullable: column.isNullable && !isPrimary,
2587
+ isAutoUpdated: column.isAutoUpdated,
2588
+ };
2589
+ if (column.default !== undefined) {
2590
+ field.default = column.default;
2591
+ }
2592
+ if (column.type === 'string' && column.maxLength !== undefined) {
2593
+ field.maxLength = column.maxLength;
2594
+ }
2595
+ if (column.type === 'decimal') {
2596
+ field.maxDigits = column.maxDigits ?? 8;
2597
+ field.decimalPlaces = column.decimalPlaces ?? 2;
2598
+ }
2599
+ if (column.unsupportedType !== undefined) {
2600
+ field.unsupportedType = column.unsupportedType;
2601
+ }
2602
+ if (column.generated !== undefined) {
2603
+ field.generated = column.generated;
2604
+ }
2605
+ // Eloquent behaviour for this column.
2606
+ if (effective !== undefined) {
2607
+ if (effective.timestamps &&
2608
+ field.type === 'dateTime' &&
2609
+ column.name === effective.updatedAt) {
2610
+ field.isAutoUpdated = true;
2611
+ }
2612
+ if (effective.timestamps &&
2613
+ field.type === 'dateTime' &&
2614
+ column.name === effective.createdAt &&
2615
+ field.default === undefined) {
2616
+ field.default = { kind: 'now' };
2617
+ }
2618
+ const generatesId = effective.uniqueIds === undefined
2619
+ ? isPrimary
2620
+ : effective.uniqueIds.includes(column.name);
2621
+ if (generatesId && field.default === undefined) {
2622
+ if (effective.usesUuids &&
2623
+ (field.type === 'uuid' || field.type === 'string')) {
2624
+ field.default = { kind: 'uuid' };
2625
+ }
2626
+ else if (effective.usesUlids && field.type === 'string') {
2627
+ field.default = { kind: 'clientGenerated', generator: 'ulid' };
2628
+ }
2629
+ }
2630
+ const cast = effective.casts.get(column.name);
2631
+ if (cast !== undefined) {
2632
+ applyCast(field, column, cast, effective, label, input);
2633
+ }
2634
+ }
2635
+ // A column that refreshes on every save is also set when the row is created, which is
2636
+ // how the other formats spell it (`auto_now`, `@updatedAt`): no separate default.
2637
+ if (field.isAutoUpdated && field.default?.kind === 'now') {
2638
+ delete field.default;
2639
+ }
2640
+ // An `enum` column without a backed enum cast becomes an enum of its own.
2641
+ if (column.enumValues !== undefined && field.enumName === undefined) {
2642
+ field.enumName = input.synthesizeEnum(input.modelName, column);
2643
+ delete field.maxLength;
2644
+ }
2645
+ if (field.enumName !== undefined) {
2646
+ if (!column.hasExplicitLength) {
2647
+ delete field.maxLength;
2648
+ }
2649
+ convertEnumDefault(field, input.enums.get(field.enumName), label, warnings);
2650
+ }
2651
+ return field;
2652
+ }
2653
+ function convertEnumDefault(field, enumeration, label, warnings) {
2654
+ const current = field.default;
2655
+ if (enumeration === undefined ||
2656
+ current === undefined ||
2657
+ current.kind !== 'literal') {
2658
+ return;
2659
+ }
2660
+ const match = enumeration.values.find((value) => value.dbValue === String(current.value));
2661
+ if (match === undefined) {
2662
+ warnings.push(`${label}: the default "${String(current.value)}" is not a value of the enum "${enumeration.name}"; it was kept as a literal.`);
2663
+ return;
2664
+ }
2665
+ field.default = { kind: 'enumValue', value: match.name };
2666
+ }
2667
+ function applyCast(field, column, cast, effective, label, input) {
2668
+ if (cast.kind === 'classRef' ||
2669
+ (cast.kind === 'string' && /\\/.test(cast.value))) {
2670
+ const className = cast.kind === 'classRef'
2671
+ ? cast.name
2672
+ : cast.kind === 'string'
2673
+ ? cast.value
2674
+ : '';
2675
+ const info = input.findEnum(className, effective.source);
2676
+ if (info === undefined) {
2677
+ return;
2678
+ }
2679
+ if (info.backing !== 'string') {
2680
+ input.warnings.push(`${label}: the cast to the ${info.backing === 'int' ? 'int-backed' : 'non-backed'} enum ${info.name} is not supported; the column keeps its ${field.type} type.`);
2681
+ return;
2682
+ }
2683
+ if (field.type !== 'string' && field.type !== 'text') {
2684
+ input.warnings.push(`${label}: the cast to the enum ${info.name} was ignored because the column is of type ${field.type}.`);
2685
+ return;
2686
+ }
2687
+ if (column.enumValues !== undefined) {
2688
+ const allowed = info.values.map((value) => value.dbValue);
2689
+ const differing = column.enumValues.filter((value) => !allowed.includes(value));
2690
+ if (differing.length > 0) {
2691
+ input.warnings.push(`${label}: the enum() values (${differing.join(', ')}) are not cases of ${info.name}.`);
2692
+ }
2693
+ }
2694
+ input.registerEnum(info);
2695
+ field.type = 'string';
2696
+ field.enumName = info.name;
2697
+ return;
2698
+ }
2699
+ if (cast.kind !== 'string') {
2700
+ return;
2701
+ }
2702
+ const [base, parameter] = cast.value.toLowerCase().split(':', 2);
2703
+ switch (base) {
2704
+ case 'boolean':
2705
+ case 'bool':
2706
+ if (field.type === 'int' || field.type === 'bigInt') {
2707
+ field.type = 'boolean';
2708
+ convertBooleanDefault(field);
2709
+ }
2710
+ break;
2711
+ case 'array':
2712
+ case 'json':
2713
+ case 'object':
2714
+ case 'collection':
2715
+ if (field.type === 'string' || field.type === 'text') {
2716
+ field.type = 'json';
2717
+ delete field.maxLength;
2718
+ }
2719
+ break;
2720
+ case 'decimal': {
2721
+ const places = Number(parameter);
2722
+ if (field.type === 'decimal' &&
2723
+ parameter !== undefined &&
2724
+ Number.isInteger(places)) {
2725
+ field.decimalPlaces = places;
2726
+ }
2727
+ break;
2728
+ }
2729
+ case 'date':
2730
+ case 'immutable_date':
2731
+ if (field.type === 'string' || field.type === 'text') {
2732
+ field.type = 'date';
2733
+ delete field.maxLength;
2734
+ }
2735
+ break;
2736
+ case 'datetime':
2737
+ case 'immutable_datetime':
2738
+ case 'custom_datetime':
2739
+ case 'immutable_custom_datetime':
2740
+ if (field.type === 'string' || field.type === 'text') {
2741
+ field.type = 'dateTime';
2742
+ delete field.maxLength;
2743
+ }
2744
+ break;
2745
+ default:
2746
+ break;
2747
+ }
2748
+ }
2749
+ function convertBooleanDefault(field) {
2750
+ if (field.default?.kind === 'literal') {
2751
+ const value = field.default.value;
2752
+ if (value === 0 || value === 1) {
2753
+ field.default = { kind: 'literal', value: value === 1 };
2754
+ }
2755
+ }
2756
+ }
2757
+ function addManyToMany(models, uses, folded, modelNames, live, warnings) {
2758
+ const consumed = new Set();
2759
+ const ordered = uses
2760
+ .filter((use) => folded.has(use.table))
2761
+ .sort((first, second) => {
2762
+ const firstKey = `${first.owner.source.name}.${first.method.name}`;
2763
+ const secondKey = `${second.owner.source.name}.${second.method.name}`;
2764
+ return firstKey < secondKey ? -1 : firstKey > secondKey ? 1 : 0;
2765
+ });
2766
+ for (const use of ordered) {
2767
+ if (consumed.has(use)) {
2768
+ continue;
2769
+ }
2770
+ consumed.add(use);
2771
+ const mirror = ordered.find((candidate) => !consumed.has(candidate) &&
2772
+ candidate.table === use.table &&
2773
+ candidate.owner === use.related &&
2774
+ candidate.related === use.owner &&
2775
+ candidate.ownerColumn === use.relatedColumn &&
2776
+ candidate.relatedColumn === use.ownerColumn);
2777
+ if (mirror !== undefined) {
2778
+ consumed.add(mirror);
2779
+ }
2780
+ const ownerModel = models.find((model) => model.tableName === use.owner.table);
2781
+ const ownerName = modelNames.get(use.owner.table) ?? use.owner.table;
2782
+ const relatedName = modelNames.get(use.related.table) ?? use.related.table;
2783
+ if (ownerModel === undefined) {
2784
+ continue;
2785
+ }
2786
+ const relation = {
2787
+ name: use.method.name,
2788
+ kind: 'manyToMany',
2789
+ targetModel: relatedName,
2790
+ columnName: `${toSnakeCase(use.method.name)}_id`,
2791
+ isNullable: false,
2792
+ onDelete: 'cascade',
2793
+ ...(mirror === undefined ? {} : { relatedName: mirror.method.name }),
2794
+ };
2795
+ ownerModel.relations.push(relation);
2796
+ // Join tables are derived by the other formats; say what does not survive.
2797
+ const isSelf = use.owner === use.related;
2798
+ const expectedTable = `${use.owner.table}_${use.method.name}`;
2799
+ const expectedOwner = `${isSelf ? 'from_' : ''}${toSnakeCase(ownerName)}_id`;
2800
+ const expectedRelated = `${isSelf ? 'to_' : ''}${toSnakeCase(relatedName)}_id`;
2801
+ if (use.table !== expectedTable ||
2802
+ use.ownerColumn !== expectedOwner ||
2803
+ use.relatedColumn !== expectedRelated) {
2804
+ warnings.push(`${ownerName}.${use.method.name}: the pivot table "${use.table}" (columns ${use.ownerColumn}, ${use.relatedColumn}) is not preserved by name; ` +
2805
+ `formats that derive join tables will use "${expectedTable}" (columns ${expectedOwner}, ${expectedRelated}).`);
2806
+ }
2807
+ const pivot = live.get(use.table);
2808
+ if (pivot !== undefined) {
2809
+ if (pivot.columns.has('created_at') || pivot.columns.has('updated_at')) {
2810
+ warnings.push(`${ownerName}.${use.method.name}: the timestamp columns of the pivot table "${use.table}" are not represented in the many-to-many field.`);
2811
+ }
2812
+ const customActions = pivot.foreignKeys
2813
+ .filter((key) => key.onDelete !== undefined &&
2814
+ key.onDelete !== 'cascade' &&
2815
+ (sameList(key.columns, [use.ownerColumn]) ||
2816
+ sameList(key.columns, [use.relatedColumn])))
2817
+ .map((key) => key.columns.join(', '));
2818
+ if (customActions.length > 0) {
2819
+ warnings.push(`${ownerName}.${use.method.name}: the foreign keys of the pivot table "${use.table}" on (${customActions.join('; ')}) do not cascade on delete; many-to-many join tables always do.`);
2820
+ }
2821
+ }
2822
+ }
2823
+ }
2824
+ //# sourceMappingURL=laravel.js.map