ormbridge 0.3.0 → 0.4.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 (46) hide show
  1. package/README.md +186 -37
  2. package/dist/cliRunner.js +1 -1
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/emitters/drizzle.d.ts +19 -0
  5. package/dist/emitters/drizzle.js +1337 -0
  6. package/dist/emitters/drizzle.js.map +1 -0
  7. package/dist/emitters/jsonSchema.d.ts +20 -0
  8. package/dist/emitters/jsonSchema.js +503 -0
  9. package/dist/emitters/jsonSchema.js.map +1 -0
  10. package/dist/emitters/sqlDdl.d.ts +29 -0
  11. package/dist/emitters/sqlDdl.js +1741 -0
  12. package/dist/emitters/sqlDdl.js.map +1 -0
  13. package/dist/emitters/typeorm.js +53 -1
  14. package/dist/emitters/typeorm.js.map +1 -1
  15. package/dist/emitters/zod.d.ts +40 -0
  16. package/dist/emitters/zod.js +498 -0
  17. package/dist/emitters/zod.js.map +1 -0
  18. package/dist/formats.d.ts +4 -1
  19. package/dist/formats.js +62 -0
  20. package/dist/formats.js.map +1 -1
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +7 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/io.js +56 -8
  25. package/dist/io.js.map +1 -1
  26. package/dist/parsers/drizzle.d.ts +12 -0
  27. package/dist/parsers/drizzle.js +2040 -0
  28. package/dist/parsers/drizzle.js.map +1 -0
  29. package/dist/parsers/jsonSchema.d.ts +21 -0
  30. package/dist/parsers/jsonSchema.js +1432 -0
  31. package/dist/parsers/jsonSchema.js.map +1 -0
  32. package/dist/parsers/sqlDdl.d.ts +18 -0
  33. package/dist/parsers/sqlDdl.js +2302 -0
  34. package/dist/parsers/sqlDdl.js.map +1 -0
  35. package/dist/parsers/sqlSyntax.d.ts +79 -0
  36. package/dist/parsers/sqlSyntax.js +698 -0
  37. package/dist/parsers/sqlSyntax.js.map +1 -0
  38. package/dist/parsers/sqlTypes.d.ts +53 -0
  39. package/dist/parsers/sqlTypes.js +742 -0
  40. package/dist/parsers/sqlTypes.js.map +1 -0
  41. package/dist/parsers/typeorm.js +22 -7
  42. package/dist/parsers/typeorm.js.map +1 -1
  43. package/dist/prismaOnlyConstructs.d.ts +2 -2
  44. package/dist/prismaOnlyConstructs.js +5 -5
  45. package/dist/prismaOnlyConstructs.js.map +1 -1
  46. package/package.json +2 -1
@@ -0,0 +1,1741 @@
1
+ import { findModel } from '../ir.js';
2
+ import { limitIdentifier, toSnakeCase } from '../naming.js';
3
+ import { expandManyToMany, normalizeSchema, } from '../transforms.js';
4
+ /** The dialect a provider is written in. MongoDB has no SQL; CockroachDB speaks PostgreSQL. */
5
+ export function sqlDialectOf(provider) {
6
+ switch (provider) {
7
+ case 'mysql':
8
+ case 'sqlite':
9
+ case 'sqlserver':
10
+ return provider;
11
+ default:
12
+ return 'postgresql';
13
+ }
14
+ }
15
+ const DIALECT_LABELS = {
16
+ postgresql: 'PostgreSQL',
17
+ mysql: 'MySQL',
18
+ sqlite: 'SQLite',
19
+ sqlserver: 'SQL Server',
20
+ };
21
+ /** Longest identifier each dialect accepts (SQLite has no limit; 128 keeps generated names sensible). */
22
+ const MAX_IDENTIFIER = {
23
+ postgresql: 63,
24
+ mysql: 64,
25
+ sqlite: 128,
26
+ sqlserver: 128,
27
+ };
28
+ // ---------------------------------------------------------------------------
29
+ // Identifiers
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Words that cannot be used as a bare identifier in at least one of the four dialects (the union of
33
+ * their reserved lists, plus the common names that are keywords in some of them). Quoting a name that
34
+ * is not reserved is harmless, so one shared list keeps the output portable.
35
+ */
36
+ const RESERVED_WORDS = new Set([
37
+ // PostgreSQL
38
+ `all analyse analyze and any array as asc asymmetric authorization binary both case cast check
39
+ collate collation column concurrently constraint create cross current_catalog current_date
40
+ current_role current_schema current_time current_timestamp current_user default deferrable desc
41
+ distinct do else end except false fetch for foreign freeze from full grant group having ilike in
42
+ initially inner intersect into is isnull join lateral leading left like limit localtime
43
+ localtimestamp natural not notnull null offset on only or order outer overlaps placing primary
44
+ references returning right select session_user similar some symmetric system_user table
45
+ tablesample then to trailing true union unique user using variadic verbose when where window with`,
46
+ // MySQL and MariaDB
47
+ `accessible add alter asensitive before bigint blob call change char character condition continue
48
+ convert cube cume_dist database databases day_hour day_microsecond day_minute day_second dec
49
+ decimal declare delayed delete dense_rank describe deterministic distinctrow div double dual each
50
+ elseif empty enclosed escaped exists exit explain first_value float float4 float8 force fulltext
51
+ function generated get groups high_priority hour_microsecond hour_minute hour_second if ignore
52
+ index infile inout insensitive insert int int1 int2 int3 int4 int8 integer interval
53
+ io_after_gtids io_before_gtids iterate json_table key keys kill lag last_value lead leave
54
+ linear lines load lock long longblob longtext loop low_priority master_bind
55
+ master_ssl_verify_server_cert match maxvalue mediumblob mediumint mediumtext middleint
56
+ minute_microsecond minute_second mod modifies no_write_to_binlog nth_value ntile numeric
57
+ optimize optimizer_costs option optionally out outfile over partition percent_rank precision
58
+ procedure purge range rank read read_write reads real recursive regexp release rename repeat
59
+ replace require resignal restrict return revoke rlike row row_number rows schema schemas
60
+ second_microsecond sensitive separator set show signal smallint spatial specific sql sqlexception
61
+ sqlstate sqlwarning sql_big_result sql_calc_found_rows sql_small_result ssl starting stored
62
+ straight_join terminated tinyblob tinyint tinytext undo unlock unsigned update usage use
63
+ utc_date utc_time utc_timestamp values varbinary varchar varcharacter varying virtual while write
64
+ xor year_month zerofill`,
65
+ // SQLite
66
+ `abort action after always attach autoincrement begin by cascade commit conflict current
67
+ deferred detach exclude exclusive fail filter first following glob immediate indexed instead
68
+ materialized no nothing nulls others pragma preceding query raise reindex rollback savepoint temp
69
+ temporary ties transaction trigger unbounded vacuum view without`,
70
+ // SQL Server
71
+ `backup break browse bulk checkpoint close clustered coalesce commit compute contains
72
+ containstable dbcc deallocate deny disk distributed dump errlvl exec execute external file
73
+ fillfactor freetext freetexttable goto holdlock identity identity_insert identitycol lineno merge
74
+ national nocheck nonclustered nullif of off offsets opendatasource openquery openrowset openxml
75
+ pivot plan print proc public raiserror readtext reconfigure replication restore revert rowcount
76
+ rowguidcol rule save securityaudit semantickeyphrasetable semanticsimilaritydetailstable
77
+ semanticsimilaritytable setuser shutdown statistics textsize top tran truncate try_convert
78
+ tsequal unpivot updatetext waitfor within writetext`,
79
+ // Type names, which several dialects refuse as a column name
80
+ `bigserial binary bit bool boolean char date datetime datetime2 enum json jsonb money nchar
81
+ ntext nvarchar serial smalldatetime smallmoney text time timestamp timestamptz uuid xml year`,
82
+ ]
83
+ .join(' ')
84
+ .split(/\s+/)
85
+ .filter((word) => word !== ''));
86
+ const BARE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
87
+ /** Quotes an identifier the way the dialect requires, and only when it has to be quoted. */
88
+ export function quoteIdentifier(dialect, name) {
89
+ if (BARE_IDENTIFIER.test(name) && !RESERVED_WORDS.has(name)) {
90
+ return name;
91
+ }
92
+ return quoteAlways(dialect, name);
93
+ }
94
+ function quoteAlways(dialect, name) {
95
+ switch (dialect) {
96
+ case 'mysql':
97
+ return `\`${name.replace(/`/g, '``')}\``;
98
+ case 'sqlserver':
99
+ return `[${name.replace(/\]/g, ']]')}]`;
100
+ default:
101
+ return `"${name.replace(/"/g, '""')}"`;
102
+ }
103
+ }
104
+ /** A string literal. MySQL also treats the backslash as an escape character. */
105
+ export function sqlStringLiteral(dialect, value) {
106
+ const body = dialect === 'mysql'
107
+ ? value.replace(/\\/g, '\\\\').replace(/'/g, "''")
108
+ : value.replace(/'/g, "''");
109
+ return `${dialect === 'sqlserver' ? 'N' : ''}'${body}'`;
110
+ }
111
+ /** Text that is safe inside a `--` comment (one line). */
112
+ function commentText(text) {
113
+ return text.replace(/\s+/g, ' ').trim();
114
+ }
115
+ // ---------------------------------------------------------------------------
116
+ // Native column types (Prisma `@db.*`), by dialect
117
+ // ---------------------------------------------------------------------------
118
+ const NATIVE_TYPES = {
119
+ postgresql: {
120
+ smallint: 'SMALLINT',
121
+ integer: 'INTEGER',
122
+ bigint: 'BIGINT',
123
+ decimal: 'NUMERIC',
124
+ money: 'MONEY',
125
+ real: 'REAL',
126
+ doubleprecision: 'DOUBLE PRECISION',
127
+ varchar: 'VARCHAR',
128
+ char: 'CHAR',
129
+ text: 'TEXT',
130
+ bytea: 'BYTEA',
131
+ timestamp: 'TIMESTAMP',
132
+ timestamptz: 'TIMESTAMPTZ',
133
+ date: 'DATE',
134
+ time: 'TIME',
135
+ timetz: 'TIMETZ',
136
+ boolean: 'BOOLEAN',
137
+ bit: 'BIT',
138
+ varbit: 'VARBIT',
139
+ uuid: 'UUID',
140
+ xml: 'XML',
141
+ inet: 'INET',
142
+ json: 'JSON',
143
+ jsonb: 'JSONB',
144
+ oid: 'OID',
145
+ },
146
+ mysql: {
147
+ int: 'INT',
148
+ unsignedint: 'INT UNSIGNED',
149
+ smallint: 'SMALLINT',
150
+ unsignedsmallint: 'SMALLINT UNSIGNED',
151
+ tinyint: 'TINYINT',
152
+ unsignedtinyint: 'TINYINT UNSIGNED',
153
+ mediumint: 'MEDIUMINT',
154
+ unsignedmediumint: 'MEDIUMINT UNSIGNED',
155
+ bigint: 'BIGINT',
156
+ unsignedbigint: 'BIGINT UNSIGNED',
157
+ float: 'FLOAT',
158
+ double: 'DOUBLE',
159
+ decimal: 'DECIMAL',
160
+ varchar: 'VARCHAR',
161
+ char: 'CHAR',
162
+ tinytext: 'TINYTEXT',
163
+ text: 'TEXT',
164
+ mediumtext: 'MEDIUMTEXT',
165
+ longtext: 'LONGTEXT',
166
+ bit: 'BIT',
167
+ binary: 'BINARY',
168
+ varbinary: 'VARBINARY',
169
+ tinyblob: 'TINYBLOB',
170
+ blob: 'BLOB',
171
+ mediumblob: 'MEDIUMBLOB',
172
+ longblob: 'LONGBLOB',
173
+ date: 'DATE',
174
+ time: 'TIME',
175
+ datetime: 'DATETIME',
176
+ timestamp: 'TIMESTAMP',
177
+ year: 'YEAR',
178
+ json: 'JSON',
179
+ },
180
+ sqlite: {},
181
+ sqlserver: {
182
+ tinyint: 'TINYINT',
183
+ smallint: 'SMALLINT',
184
+ int: 'INT',
185
+ bigint: 'BIGINT',
186
+ decimal: 'DECIMAL',
187
+ numeric: 'NUMERIC',
188
+ money: 'MONEY',
189
+ smallmoney: 'SMALLMONEY',
190
+ float: 'FLOAT',
191
+ real: 'REAL',
192
+ bit: 'BIT',
193
+ char: 'CHAR',
194
+ nchar: 'NCHAR',
195
+ varchar: 'VARCHAR',
196
+ nvarchar: 'NVARCHAR',
197
+ // TEXT, NTEXT and IMAGE are deprecated in SQL Server and cannot be compared or indexed.
198
+ text: 'NVARCHAR(MAX)',
199
+ ntext: 'NVARCHAR(MAX)',
200
+ xml: 'XML',
201
+ binary: 'BINARY',
202
+ varbinary: 'VARBINARY',
203
+ image: 'VARBINARY(MAX)',
204
+ date: 'DATE',
205
+ time: 'TIME',
206
+ datetime: 'DATETIME',
207
+ datetime2: 'DATETIME2',
208
+ smalldatetime: 'SMALLDATETIME',
209
+ datetimeoffset: 'DATETIMEOFFSET',
210
+ uniqueidentifier: 'UNIQUEIDENTIFIER',
211
+ },
212
+ };
213
+ const POSTGRES_RANGE_TYPES = {
214
+ int: 'INT4RANGE',
215
+ bigInt: 'INT8RANGE',
216
+ decimal: 'NUMRANGE',
217
+ date: 'DATERANGE',
218
+ dateTime: 'TSTZRANGE',
219
+ };
220
+ const NOW_DEFAULTS = {
221
+ postgresql: {
222
+ dateTime: 'now()',
223
+ date: 'CURRENT_DATE',
224
+ time: 'CURRENT_TIME',
225
+ },
226
+ mysql: {
227
+ dateTime: 'CURRENT_TIMESTAMP(6)',
228
+ date: '(CURRENT_DATE)',
229
+ time: '(CURRENT_TIME)',
230
+ },
231
+ sqlite: {
232
+ dateTime: 'CURRENT_TIMESTAMP',
233
+ date: 'CURRENT_DATE',
234
+ time: 'CURRENT_TIME',
235
+ },
236
+ sqlserver: {
237
+ dateTime: 'SYSUTCDATETIME()',
238
+ date: '(CAST(SYSUTCDATETIME() AS DATE))',
239
+ time: '(CAST(SYSUTCDATETIME() AS TIME))',
240
+ },
241
+ };
242
+ /** A version 4 UUID built from SQLite's random bytes (SQLite has no uuid function). */
243
+ const SQLITE_UUID_EXPRESSION = "(lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || " +
244
+ "substr(lower(hex(randomblob(2))), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || " +
245
+ "substr(lower(hex(randomblob(2))), 2) || '-' || lower(hex(randomblob(6))))";
246
+ /** MySQL wants CURRENT_TIMESTAMP(n) to have the fractional seconds of the column it fills. */
247
+ function mysqlCurrentTimestamp(columnType) {
248
+ const precision = /\((\d)\)/.exec(columnType);
249
+ return precision === null || precision[1] === '0'
250
+ ? 'CURRENT_TIMESTAMP'
251
+ : `CURRENT_TIMESTAMP(${precision[1] ?? '6'})`;
252
+ }
253
+ const UUID_DEFAULTS = {
254
+ postgresql: 'gen_random_uuid()',
255
+ mysql: '(UUID())',
256
+ sqlite: SQLITE_UUID_EXPRESSION,
257
+ sqlserver: 'NEWID()',
258
+ };
259
+ // ---------------------------------------------------------------------------
260
+ // Entry point
261
+ // ---------------------------------------------------------------------------
262
+ export function emitSqlDdl(schema, options) {
263
+ // The join table of a many-to-many field is an ordinary table in SQL.
264
+ const expanded = expandManyToMany(schema);
265
+ const prepared = options.naming === 'normalize' ? normalizeSchema(expanded) : expanded;
266
+ const dialect = sqlDialectOf(options.provider);
267
+ const context = {
268
+ dialect,
269
+ schema: prepared,
270
+ warnings: [],
271
+ enums: new Map(),
272
+ names: new Set(),
273
+ unexpressedAutoUpdates: [],
274
+ needsHstore: false,
275
+ };
276
+ warnAboutProvider(context, options.provider);
277
+ warnAboutIgnoredConstructs(context);
278
+ resolveEnums(context, options.naming);
279
+ const tables = [];
280
+ const views = [];
281
+ for (const model of prepared.models) {
282
+ if (model.isView === true) {
283
+ views.push(model);
284
+ }
285
+ else {
286
+ tables.push(buildTable(context, model));
287
+ }
288
+ }
289
+ const ordered = orderTables(context, tables);
290
+ if (dialect === 'sqlserver') {
291
+ limitCascadePaths(context, ordered);
292
+ }
293
+ for (const table of ordered) {
294
+ buildIndexes(context, table);
295
+ }
296
+ for (const view of views) {
297
+ context.warnings.push(`${view.name}: this is a database view; the schema has no SQL definition for it, so only a commented placeholder was written.`);
298
+ }
299
+ if (context.unexpressedAutoUpdates.length > 0) {
300
+ context.warnings.push(`${DIALECT_LABELS[dialect]} cannot refresh a column on update from the DDL (${context.unexpressedAutoUpdates.join(', ')}); the application or a trigger has to set it.`);
301
+ }
302
+ return { text: render(context, ordered, views), warnings: context.warnings };
303
+ }
304
+ function warnAboutProvider(context, provider) {
305
+ if (provider === 'mongodb') {
306
+ context.warnings.push('Provider "mongodb": MongoDB has no SQL schema; PostgreSQL DDL was written.');
307
+ }
308
+ else if (provider === 'cockroachdb') {
309
+ context.warnings.push('Provider "cockroachdb": PostgreSQL DDL was written, which CockroachDB accepts; check the types it maps differently.');
310
+ }
311
+ }
312
+ function warnAboutIgnoredConstructs(context) {
313
+ for (const definition of context.schema.enums) {
314
+ if (definition.schema !== undefined) {
315
+ context.warnings.push(`enum ${definition.name}: the database schema "${definition.schema}" (@@schema) was ignored; the type was created in the default schema.`);
316
+ }
317
+ }
318
+ for (const model of context.schema.models) {
319
+ if (model.schema !== undefined) {
320
+ context.warnings.push(`${model.name}: the database schema "${model.schema}" (@@schema) was ignored; the table was created in the default schema.`);
321
+ }
322
+ }
323
+ }
324
+ // ---------------------------------------------------------------------------
325
+ // Names
326
+ // ---------------------------------------------------------------------------
327
+ function quote(context, name) {
328
+ return quoteIdentifier(context.dialect, name);
329
+ }
330
+ function quoteList(context, names) {
331
+ return names.map((name) => quote(context, name)).join(', ');
332
+ }
333
+ /** Reserves a constraint or index name; a name that is already taken gets a numeric suffix. */
334
+ function claimName(context, wanted) {
335
+ const max = MAX_IDENTIFIER[context.dialect];
336
+ let candidate = wanted;
337
+ let suffix = 2;
338
+ while (context.names.has(candidate.toLowerCase())) {
339
+ candidate = `${limitIdentifier(wanted, max - 3)}_${suffix}`;
340
+ suffix += 1;
341
+ }
342
+ context.names.add(candidate.toLowerCase());
343
+ return candidate;
344
+ }
345
+ function generatedName(context, table, columns, suffix) {
346
+ return claimName(context, limitIdentifier(`${table}_${columns.join('_')}_${suffix}`, MAX_IDENTIFIER[context.dialect]));
347
+ }
348
+ function checkIdentifierLength(context, label, name) {
349
+ const max = MAX_IDENTIFIER[context.dialect];
350
+ if (context.dialect !== 'sqlite' && name.length > max) {
351
+ context.warnings.push(`${label}: the name "${name}" is longer than the ${max} characters ${DIALECT_LABELS[context.dialect]} allows in an identifier.`);
352
+ }
353
+ }
354
+ function resolveEnums(context, naming) {
355
+ for (const definition of context.schema.enums) {
356
+ if (definition.values.length === 0) {
357
+ context.warnings.push(`enum ${definition.name}: an enum without values cannot be written; columns that use it became plain strings.`);
358
+ continue;
359
+ }
360
+ const dbName = definition.dbName ?? definition.name;
361
+ let name = naming === 'normalize' ? toSnakeCase(dbName) : dbName;
362
+ if (context.dialect === 'postgresql') {
363
+ // A PostgreSQL type shares its namespace with the tables (every table is also a row type).
364
+ const taken = new Set(context.schema.models.map((model) => model.tableName));
365
+ for (const other of context.enums.values()) {
366
+ taken.add(other.dbName);
367
+ }
368
+ if (taken.has(name)) {
369
+ const renamed = `${name}_enum`;
370
+ context.warnings.push(`enum ${definition.name}: the type name "${name}" is already used by a table or another type; the type was named "${renamed}".`);
371
+ name = renamed;
372
+ }
373
+ }
374
+ context.enums.set(definition.name, { definition, dbName: name });
375
+ }
376
+ }
377
+ function enumOf(context, field) {
378
+ return field.enumName === undefined
379
+ ? undefined
380
+ : context.enums.get(field.enumName);
381
+ }
382
+ // ---------------------------------------------------------------------------
383
+ // Column types
384
+ // ---------------------------------------------------------------------------
385
+ function sizedType(base, args) {
386
+ const given = [];
387
+ for (const arg of args) {
388
+ if (arg !== undefined) {
389
+ given.push(arg);
390
+ }
391
+ }
392
+ return given.length === 0 ? base : `${base}(${given.join(', ')})`;
393
+ }
394
+ /** The SQL type for a Prisma `@db.*` native type, when the dialect has that type. */
395
+ function nativeTypeSql(dialect, field) {
396
+ if (field.nativeType === undefined) {
397
+ return undefined;
398
+ }
399
+ const known = NATIVE_TYPES[dialect][field.nativeType.name.toLowerCase()];
400
+ if (known === undefined) {
401
+ return undefined;
402
+ }
403
+ const args = field.nativeType.args.map((arg) => /^max$/i.test(arg) ? 'MAX' : arg);
404
+ if (args.length === 0) {
405
+ return known;
406
+ }
407
+ // "INT UNSIGNED" style names put the size after the first word.
408
+ const [first, ...rest] = known.split(' ');
409
+ return `${first ?? known}(${args.join(', ')})${rest.length > 0 ? ` ${rest.join(' ')}` : ''}`;
410
+ }
411
+ function storageOf(type) {
412
+ switch (type) {
413
+ case 'text':
414
+ return 'text';
415
+ case 'bytes':
416
+ return 'blob';
417
+ case 'json':
418
+ case 'hstore':
419
+ return 'json';
420
+ default:
421
+ return 'other';
422
+ }
423
+ }
424
+ function plainType(sql, field) {
425
+ return {
426
+ sql,
427
+ checks: [],
428
+ storage: storageOf(field.type),
429
+ unindexable: false,
430
+ textual: false,
431
+ };
432
+ }
433
+ /** The column type of a field, with the CHECK constraints and notes that go with it. */
434
+ function columnType(context, label, field, columnSql) {
435
+ const depth = field.arrayDepth ?? 0;
436
+ const element = scalarColumnType(context, label, { ...field, arrayDepth: undefined }, columnSql, depth > 0);
437
+ if (depth === 0) {
438
+ return element;
439
+ }
440
+ if (context.dialect === 'postgresql') {
441
+ return {
442
+ ...element,
443
+ sql: `${element.sql}${'[]'.repeat(depth)}`,
444
+ storage: 'other',
445
+ };
446
+ }
447
+ const label2 = DIALECT_LABELS[context.dialect];
448
+ context.warnings.push(`${label}: ${label2} has no array column type; the array was written as a JSON ${context.dialect === 'sqlite' ? 'text ' : ''}column.`);
449
+ return jsonColumnType(context, columnSql, 'an array stored as JSON');
450
+ }
451
+ function jsonColumnType(context, columnSql, note) {
452
+ const base = {
453
+ storage: 'json',
454
+ textual: true,
455
+ };
456
+ switch (context.dialect) {
457
+ case 'postgresql':
458
+ return {
459
+ ...base,
460
+ sql: 'JSONB',
461
+ checks: [],
462
+ unindexable: false,
463
+ ...(note === undefined ? {} : { comment: note }),
464
+ };
465
+ case 'mysql':
466
+ return {
467
+ ...base,
468
+ sql: 'JSON',
469
+ checks: [],
470
+ // MySQL cannot index a JSON column directly.
471
+ unindexable: true,
472
+ ...(note === undefined ? {} : { comment: note }),
473
+ };
474
+ case 'sqlite':
475
+ return {
476
+ ...base,
477
+ sql: 'TEXT',
478
+ checks: [`json_valid(${columnSql})`],
479
+ unindexable: false,
480
+ ...(note === undefined ? {} : { comment: note }),
481
+ };
482
+ default:
483
+ return {
484
+ ...base,
485
+ sql: 'NVARCHAR(MAX)',
486
+ checks: [`ISJSON(${columnSql}) = 1`],
487
+ unindexable: true,
488
+ ...(note === undefined ? {} : { comment: note }),
489
+ };
490
+ }
491
+ }
492
+ function scalarColumnType(context, label, field, columnSql, isArrayElement) {
493
+ const dialect = context.dialect;
494
+ const enumInfo = enumOf(context, field);
495
+ if (field.enumName !== undefined && enumInfo === undefined) {
496
+ context.warnings.push(`${label}: enum "${field.enumName}" does not exist in the schema; the column was written as a plain string.`);
497
+ }
498
+ if (enumInfo !== undefined) {
499
+ return enumColumnType(context, enumInfo, columnSql, isArrayElement);
500
+ }
501
+ const native = nativeTypeSql(dialect, field);
502
+ if (native !== undefined) {
503
+ return {
504
+ ...plainType(native, field),
505
+ textual: /CHAR|TEXT|UUID|UNIQUEIDENTIFIER|JSON|XML/i.test(native),
506
+ unindexable: /\(MAX\)$/i.test(native) || /^(TEXT|NTEXT|IMAGE)$/i.test(native),
507
+ };
508
+ }
509
+ switch (field.type) {
510
+ case 'string':
511
+ return stringColumnType(context, label, field);
512
+ case 'text':
513
+ return {
514
+ sql: dialect === 'mysql'
515
+ ? 'LONGTEXT'
516
+ : dialect === 'sqlserver'
517
+ ? 'NVARCHAR(MAX)'
518
+ : 'TEXT',
519
+ checks: [],
520
+ storage: 'text',
521
+ unindexable: dialect === 'sqlserver',
522
+ textual: true,
523
+ };
524
+ case 'uuid':
525
+ return {
526
+ sql: dialect === 'postgresql'
527
+ ? 'UUID'
528
+ : dialect === 'sqlserver'
529
+ ? 'UNIQUEIDENTIFIER'
530
+ : 'CHAR(36)',
531
+ checks: [],
532
+ storage: 'other',
533
+ unindexable: false,
534
+ textual: true,
535
+ };
536
+ case 'int':
537
+ return plainType(dialect === 'mysql' || dialect === 'sqlserver' ? 'INT' : 'INTEGER', field);
538
+ case 'bigInt':
539
+ return plainType('BIGINT', field);
540
+ case 'float':
541
+ return plainType(dialect === 'postgresql'
542
+ ? 'DOUBLE PRECISION'
543
+ : dialect === 'sqlite'
544
+ ? 'REAL'
545
+ : dialect === 'mysql'
546
+ ? 'DOUBLE'
547
+ : 'FLOAT', field);
548
+ case 'decimal':
549
+ return plainType(decimalType(context, label, field), field);
550
+ case 'boolean':
551
+ return plainType(dialect === 'sqlserver' ? 'BIT' : 'BOOLEAN', field);
552
+ case 'dateTime':
553
+ return plainType(dialect === 'postgresql'
554
+ ? 'TIMESTAMPTZ'
555
+ : dialect === 'mysql'
556
+ ? 'DATETIME(6)'
557
+ : dialect === 'sqlite'
558
+ ? 'DATETIME'
559
+ : 'DATETIME2', field);
560
+ case 'date':
561
+ return plainType('DATE', field);
562
+ case 'time':
563
+ return plainType(dialect === 'mysql' ? 'TIME(6)' : 'TIME', field);
564
+ case 'json':
565
+ return jsonColumnType(context, columnSql);
566
+ case 'bytes':
567
+ return {
568
+ sql: dialect === 'postgresql'
569
+ ? 'BYTEA'
570
+ : dialect === 'mysql'
571
+ ? 'LONGBLOB'
572
+ : dialect === 'sqlite'
573
+ ? 'BLOB'
574
+ : 'VARBINARY(MAX)',
575
+ checks: [],
576
+ storage: 'blob',
577
+ unindexable: dialect === 'sqlserver',
578
+ textual: false,
579
+ };
580
+ case 'duration':
581
+ if (dialect === 'postgresql') {
582
+ return plainType('INTERVAL', field);
583
+ }
584
+ context.warnings.push(`${label}: ${DIALECT_LABELS[dialect]} has no interval type; the duration was written as an integer (microseconds).`);
585
+ return {
586
+ ...plainType(dialect === 'sqlite' ? 'INTEGER' : 'BIGINT', field),
587
+ comment: 'duration in microseconds',
588
+ };
589
+ case 'ipAddress':
590
+ if (dialect === 'postgresql') {
591
+ return plainType('INET', field);
592
+ }
593
+ return {
594
+ sql: dialect === 'sqlite' ? 'TEXT' : 'VARCHAR(45)',
595
+ checks: [],
596
+ storage: 'other',
597
+ unindexable: false,
598
+ textual: true,
599
+ };
600
+ case 'hstore':
601
+ if (dialect === 'postgresql') {
602
+ context.needsHstore = true;
603
+ return { ...plainType('HSTORE', field), textual: true };
604
+ }
605
+ context.warnings.push(`${label}: hstore exists only on PostgreSQL; the column was written as JSON.`);
606
+ return jsonColumnType(context, columnSql, 'hstore stored as JSON');
607
+ case 'range':
608
+ return rangeColumnType(context, label, field);
609
+ case 'unsupported':
610
+ return unsupportedColumnType(context, label, field);
611
+ default:
612
+ context.warnings.push(`${label}: unknown field type "${String(field.type)}"; it was written as text.`);
613
+ return { ...plainType('TEXT', field), textual: true };
614
+ }
615
+ }
616
+ function stringColumnType(context, label, field) {
617
+ const dialect = context.dialect;
618
+ const length = field.maxLength;
619
+ const textual = (sql, storage = 'other') => ({
620
+ sql,
621
+ checks: [],
622
+ storage,
623
+ unindexable: false,
624
+ textual: true,
625
+ });
626
+ switch (dialect) {
627
+ case 'postgresql':
628
+ return textual(length === undefined ? 'TEXT' : `VARCHAR(${length})`);
629
+ case 'mysql':
630
+ if (length === undefined) {
631
+ // MySQL cannot key an unbounded text column and VARCHAR needs a length.
632
+ return textual('VARCHAR(255)');
633
+ }
634
+ if (length > 16383) {
635
+ context.warnings.push(`${label}: VARCHAR(${length}) does not fit a MySQL row; the column was written as LONGTEXT.`);
636
+ return textual('LONGTEXT', 'text');
637
+ }
638
+ return textual(`VARCHAR(${length})`);
639
+ case 'sqlite':
640
+ return textual(length === undefined ? 'TEXT' : `VARCHAR(${length})`);
641
+ default: {
642
+ if (length === undefined) {
643
+ return textual('NVARCHAR(255)');
644
+ }
645
+ if (length > 4000) {
646
+ return { ...textual('NVARCHAR(MAX)', 'text'), unindexable: true };
647
+ }
648
+ return textual(`NVARCHAR(${length})`);
649
+ }
650
+ }
651
+ }
652
+ function decimalType(context, label, field) {
653
+ const dialect = context.dialect;
654
+ let precision = field.maxDigits;
655
+ let scale = field.decimalPlaces;
656
+ if (precision === undefined) {
657
+ scale = undefined;
658
+ if (dialect === 'mysql') {
659
+ precision = 65;
660
+ scale = 30;
661
+ }
662
+ else if (dialect === 'sqlserver') {
663
+ precision = 32;
664
+ scale = 16;
665
+ }
666
+ }
667
+ const limit = dialect === 'mysql' ? 65 : dialect === 'sqlserver' ? 38 : undefined;
668
+ if (precision !== undefined && limit !== undefined && precision > limit) {
669
+ context.warnings.push(`${label}: ${DIALECT_LABELS[dialect]} allows at most ${limit} digits in a decimal; ${precision} was reduced to ${limit}.`);
670
+ precision = limit;
671
+ if (scale !== undefined && scale > limit) {
672
+ scale = limit;
673
+ }
674
+ }
675
+ if (dialect === 'mysql' && scale !== undefined && scale > 30) {
676
+ context.warnings.push(`${label}: MySQL allows at most 30 decimal places; ${scale} was reduced to 30.`);
677
+ scale = 30;
678
+ }
679
+ const base = dialect === 'postgresql'
680
+ ? 'NUMERIC'
681
+ : dialect === 'sqlite'
682
+ ? precision === undefined
683
+ ? 'NUMERIC'
684
+ : 'DECIMAL'
685
+ : 'DECIMAL';
686
+ return sizedType(base, [
687
+ precision,
688
+ precision === undefined ? undefined : scale,
689
+ ]);
690
+ }
691
+ function rangeColumnType(context, label, field) {
692
+ if (context.dialect === 'postgresql') {
693
+ return {
694
+ ...plainType(POSTGRES_RANGE_TYPES[field.rangeOf ?? 'int'] ?? 'INT4RANGE', field),
695
+ storage: 'other',
696
+ };
697
+ }
698
+ context.warnings.push(`${label}: range columns exist only on PostgreSQL; the column was written as text.`);
699
+ return {
700
+ sql: context.dialect === 'sqlite'
701
+ ? 'TEXT'
702
+ : context.dialect === 'mysql'
703
+ ? 'VARCHAR(255)'
704
+ : 'NVARCHAR(255)',
705
+ checks: [],
706
+ comment: 'range stored as text',
707
+ storage: 'other',
708
+ unindexable: false,
709
+ textual: true,
710
+ };
711
+ }
712
+ function unsupportedColumnType(context, label, field) {
713
+ if (field.unsupportedType !== undefined && field.unsupportedType !== '') {
714
+ context.warnings.push(`${label}: the database type "${field.unsupportedType}" (Prisma Unsupported) was written as it is; check that ${DIALECT_LABELS[context.dialect]} has it.`);
715
+ return {
716
+ ...plainType(field.unsupportedType, field),
717
+ storage: 'other',
718
+ textual: false,
719
+ };
720
+ }
721
+ context.warnings.push(`${label}: the column has an unsupported database type that is unknown; it was written as text.`);
722
+ return { ...plainType('TEXT', field), textual: true };
723
+ }
724
+ function enumColumnType(context, info, columnSql, isArrayElement) {
725
+ const dialect = context.dialect;
726
+ const values = info.definition.values.map((value) => sqlStringLiteral(dialect, value.dbValue));
727
+ if (dialect === 'postgresql') {
728
+ return {
729
+ sql: quote(context, info.dbName),
730
+ checks: [],
731
+ storage: 'other',
732
+ unindexable: false,
733
+ textual: true,
734
+ };
735
+ }
736
+ if (dialect === 'mysql') {
737
+ return {
738
+ sql: `ENUM(${values.join(', ')})`,
739
+ checks: [],
740
+ storage: 'other',
741
+ unindexable: false,
742
+ textual: true,
743
+ };
744
+ }
745
+ const longest = Math.max(...info.definition.values.map((value) => value.dbValue.length));
746
+ const length = Math.max(longest, 1);
747
+ return {
748
+ sql: dialect === 'sqlite'
749
+ ? `VARCHAR(${length})`
750
+ : `NVARCHAR(${Math.min(Math.max(length, 50), 4000)})`,
751
+ checks: isArrayElement ? [] : [`${columnSql} IN (${values.join(', ')})`],
752
+ storage: 'other',
753
+ unindexable: false,
754
+ textual: true,
755
+ };
756
+ }
757
+ // ---------------------------------------------------------------------------
758
+ // Defaults
759
+ // ---------------------------------------------------------------------------
760
+ function booleanLiteral(dialect, value) {
761
+ if (dialect === 'postgresql' || dialect === 'mysql') {
762
+ return value ? 'TRUE' : 'FALSE';
763
+ }
764
+ return value ? '1' : '0';
765
+ }
766
+ /** MySQL wants an expression default in parentheses for TEXT, BLOB and JSON columns. */
767
+ function wrapForColumn(context, type, literal) {
768
+ if (context.dialect === 'mysql' && type.storage !== 'other') {
769
+ return `(${literal})`;
770
+ }
771
+ return literal;
772
+ }
773
+ function parseJson(text) {
774
+ try {
775
+ return { ok: true, value: JSON.parse(text) };
776
+ }
777
+ catch {
778
+ return { ok: false };
779
+ }
780
+ }
781
+ /** Writes a JSON array as a PostgreSQL array literal body, e.g. {"a","b"}. */
782
+ function postgresArray(value) {
783
+ const items = value.map((item) => {
784
+ if (Array.isArray(item)) {
785
+ return postgresArray(item);
786
+ }
787
+ if (item === null) {
788
+ return 'NULL';
789
+ }
790
+ if (typeof item === 'number' || typeof item === 'boolean') {
791
+ return String(item);
792
+ }
793
+ const text = typeof item === 'string' ? item : JSON.stringify(item);
794
+ return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
795
+ });
796
+ return `{${items.join(',')}}`;
797
+ }
798
+ function dateTimeLiteral(dialect, value) {
799
+ const time = Date.parse(value);
800
+ if (Number.isNaN(time)) {
801
+ return undefined;
802
+ }
803
+ const iso = new Date(time).toISOString();
804
+ switch (dialect) {
805
+ case 'postgresql':
806
+ return `'${iso}'`;
807
+ case 'sqlserver':
808
+ return `'${iso.replace('Z', '')}'`;
809
+ default:
810
+ return `'${iso
811
+ .replace('T', ' ')
812
+ .replace('Z', '')
813
+ .replace(/\.000$/, '')}'`;
814
+ }
815
+ }
816
+ function enumStoredValue(info, raw) {
817
+ const value = info.definition.values.find((candidate) => candidate.name === raw) ??
818
+ info.definition.values.find((candidate) => candidate.dbValue === raw);
819
+ return value?.dbValue;
820
+ }
821
+ const UUID_EXPRESSIONS = new Set([
822
+ 'gen_random_uuid()',
823
+ 'uuid_generate_v4()',
824
+ 'uuid()',
825
+ '(uuid())',
826
+ 'newid()',
827
+ 'newsequentialid()',
828
+ ]);
829
+ const NOW_EXPRESSIONS = new Set([
830
+ 'now()',
831
+ 'current_timestamp',
832
+ 'current_timestamp()',
833
+ 'getdate()',
834
+ 'getutcdate()',
835
+ 'sysdatetime()',
836
+ 'sysutcdatetime()',
837
+ "datetime('now')",
838
+ ]);
839
+ /** The dialect's own form of a default expression that every database has under some name. */
840
+ function knownExpression(dialect, field, expression, type) {
841
+ const normalized = expression.replace(/\s+/g, '').toLowerCase();
842
+ if (UUID_EXPRESSIONS.has(normalized) && type.textual) {
843
+ return UUID_DEFAULTS[dialect];
844
+ }
845
+ if (NOW_EXPRESSIONS.has(normalized) && field.type === 'dateTime') {
846
+ return dialect === 'mysql'
847
+ ? mysqlCurrentTimestamp(type.sql)
848
+ : NOW_DEFAULTS[dialect].dateTime;
849
+ }
850
+ return undefined;
851
+ }
852
+ /** The text after DEFAULT for a field, or undefined when there is none (or it cannot be written). */
853
+ function defaultSql(context, label, field, type) {
854
+ const value = field.default;
855
+ if (value === undefined) {
856
+ return undefined;
857
+ }
858
+ const dialect = context.dialect;
859
+ const dropped = (reason) => {
860
+ context.warnings.push(`${label}: ${reason}; the default was dropped.`);
861
+ return undefined;
862
+ };
863
+ switch (value.kind) {
864
+ case 'autoIncrement':
865
+ // Handled with the column itself.
866
+ return undefined;
867
+ case 'now': {
868
+ if (field.type !== 'dateTime' &&
869
+ field.type !== 'date' &&
870
+ field.type !== 'time') {
871
+ return dropped(`a "now" default on a ${field.type} column cannot be written`);
872
+ }
873
+ return dialect === 'mysql' && field.type === 'dateTime'
874
+ ? mysqlCurrentTimestamp(type.sql)
875
+ : NOW_DEFAULTS[dialect][field.type];
876
+ }
877
+ case 'uuid':
878
+ if (field.type !== 'uuid') {
879
+ return dropped(`a UUID default on a ${field.type} column cannot be written`);
880
+ }
881
+ return UUID_DEFAULTS[dialect];
882
+ case 'clientGenerated':
883
+ return dropped(`the ${value.generator}() default is generated by the ORM client, not the database`);
884
+ case 'dbExpression': {
885
+ if (value.isFunction === true) {
886
+ return dropped(`the ${value.expression} default function has no SQL equivalent`);
887
+ }
888
+ const known = knownExpression(dialect, field, value.expression, type);
889
+ if (known !== undefined) {
890
+ return known;
891
+ }
892
+ context.warnings.push(`${label}: the database default expression ${value.expression} was written as it is; check that ${DIALECT_LABELS[dialect]} accepts it.`);
893
+ const expression = value.expression.trim();
894
+ return expression.startsWith('(') ? expression : `(${expression})`;
895
+ }
896
+ case 'enumValue': {
897
+ const info = enumOf(context, field);
898
+ const stored = info === undefined ? undefined : enumStoredValue(info, value.value);
899
+ if (stored === undefined) {
900
+ context.warnings.push(`${label}: the enum default "${value.value}" does not match a member of the enum; it was written as a string.`);
901
+ }
902
+ return sqlStringLiteral(dialect, stored ?? value.value);
903
+ }
904
+ case 'literal':
905
+ return literalDefault(context, label, field, value.value, type);
906
+ default:
907
+ return undefined;
908
+ }
909
+ }
910
+ function literalDefault(context, label, field, value, type) {
911
+ const dialect = context.dialect;
912
+ const dropped = (reason) => {
913
+ context.warnings.push(`${label}: ${reason}; the default was dropped.`);
914
+ return undefined;
915
+ };
916
+ const text = String(value);
917
+ if ((field.arrayDepth ?? 0) > 0) {
918
+ const parsed = typeof value === 'string' ? parseJson(value) : { ok: false };
919
+ if (!parsed.ok || !Array.isArray(parsed.value)) {
920
+ return dropped(`the array default ${text} cannot be written`);
921
+ }
922
+ if (dialect === 'postgresql') {
923
+ return sqlStringLiteral(dialect, postgresArray(parsed.value));
924
+ }
925
+ return wrapForColumn(context, type, sqlStringLiteral(dialect, JSON.stringify(parsed.value)));
926
+ }
927
+ const info = enumOf(context, field);
928
+ if (info !== undefined) {
929
+ return sqlStringLiteral(dialect, enumStoredValue(info, text) ?? text);
930
+ }
931
+ switch (field.type) {
932
+ case 'int':
933
+ case 'bigInt':
934
+ case 'float': {
935
+ const numeric = typeof value === 'number' ? value : Number(text);
936
+ return typeof value === 'boolean' || !Number.isFinite(numeric)
937
+ ? dropped(`the value ${text} is not a number`)
938
+ : String(numeric);
939
+ }
940
+ case 'duration': {
941
+ if (dialect === 'postgresql') {
942
+ return sqlStringLiteral(dialect, text);
943
+ }
944
+ const numeric = Number(text);
945
+ return typeof value === 'boolean' || !Number.isFinite(numeric)
946
+ ? dropped(`the value ${text} is not a number`)
947
+ : String(numeric);
948
+ }
949
+ case 'decimal':
950
+ return /^-?\d+(\.\d+)?$/.test(text)
951
+ ? text
952
+ : sqlStringLiteral(dialect, text);
953
+ case 'boolean': {
954
+ if (typeof value === 'boolean') {
955
+ return booleanLiteral(dialect, value);
956
+ }
957
+ const lower = text.toLowerCase();
958
+ if (lower === 'true' || lower === '1') {
959
+ return booleanLiteral(dialect, true);
960
+ }
961
+ return lower === 'false' || lower === '0'
962
+ ? booleanLiteral(dialect, false)
963
+ : dropped(`the value ${text} is not a boolean`);
964
+ }
965
+ case 'dateTime': {
966
+ const literal = dateTimeLiteral(dialect, text);
967
+ return literal === undefined
968
+ ? dropped(`the value ${text} is not a date`)
969
+ : literal;
970
+ }
971
+ case 'json': {
972
+ const parsed = typeof value === 'string' ? parseJson(text) : { ok: true, value };
973
+ if (!parsed.ok) {
974
+ return dropped(`the value ${text} is not JSON`);
975
+ }
976
+ const json = sqlStringLiteral(dialect, JSON.stringify(parsed.value));
977
+ return wrapForColumn(context, type, dialect === 'postgresql' ? `${json}::jsonb` : json);
978
+ }
979
+ case 'bytes':
980
+ return dropped('a literal default on a binary column cannot be written');
981
+ case 'hstore': {
982
+ if (dialect === 'postgresql') {
983
+ return text === '{}'
984
+ ? "''::hstore"
985
+ : dropped('only an empty hstore default can be written');
986
+ }
987
+ const parsed = parseJson(text);
988
+ return parsed.ok
989
+ ? wrapForColumn(context, type, sqlStringLiteral(dialect, JSON.stringify(parsed.value)))
990
+ : dropped(`the value ${text} is not JSON`);
991
+ }
992
+ case 'range':
993
+ return dropped('a range default cannot be written');
994
+ default:
995
+ return wrapForColumn(context, type, sqlStringLiteral(dialect, text));
996
+ }
997
+ }
998
+ // ---------------------------------------------------------------------------
999
+ // Referenced columns
1000
+ // ---------------------------------------------------------------------------
1001
+ function isIntegerKey(field) {
1002
+ return field.type === 'int' || field.type === 'bigInt';
1003
+ }
1004
+ /** Resolves the column a foreign key points at: `toField`, otherwise the target's single-column key. */
1005
+ function resolveTarget(context, target, toField, depth = 0) {
1006
+ if (depth > 5) {
1007
+ return undefined;
1008
+ }
1009
+ if (toField !== undefined) {
1010
+ const named = target.fields.find((candidate) => candidate.name === toField) ??
1011
+ target.fields.find((candidate) => candidate.columnName === toField);
1012
+ if (named !== undefined) {
1013
+ return { columnName: named.columnName, field: named };
1014
+ }
1015
+ const viaRelation = target.relations.find((candidate) => candidate.kind !== 'manyToMany' &&
1016
+ (candidate.name === toField || candidate.columnName === toField));
1017
+ return viaRelation === undefined
1018
+ ? undefined
1019
+ : resolveRelationColumn(context, target, viaRelation, depth);
1020
+ }
1021
+ const keyFields = target.fields.filter((candidate) => candidate.isPrimaryKey);
1022
+ const keyRelations = target.relations.filter((candidate) => candidate.kind !== 'manyToMany' && candidate.isPrimaryKey === true);
1023
+ const composite = target.compositePrimaryKey ?? [];
1024
+ if (composite.length > 1) {
1025
+ return undefined;
1026
+ }
1027
+ const onlyField = keyFields.length === 1 && keyRelations.length === 0
1028
+ ? keyFields[0]
1029
+ : composite.length === 1
1030
+ ? target.fields.find((candidate) => candidate.name === composite[0])
1031
+ : undefined;
1032
+ if (onlyField !== undefined) {
1033
+ return { columnName: onlyField.columnName, field: onlyField };
1034
+ }
1035
+ const onlyRelation = keyRelations.length === 1 && keyFields.length === 0
1036
+ ? keyRelations[0]
1037
+ : target.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.name === composite[0]);
1038
+ return onlyRelation === undefined
1039
+ ? undefined
1040
+ : resolveRelationColumn(context, target, onlyRelation, depth);
1041
+ }
1042
+ /** A relation column takes the type of the column it points at. */
1043
+ function resolveRelationColumn(context, owner, relation, depth) {
1044
+ const next = findModel(context.schema, relation.targetModel);
1045
+ if (next === undefined || next === owner) {
1046
+ return undefined;
1047
+ }
1048
+ const inner = resolveTarget(context, next, relation.toField, depth + 1);
1049
+ return inner === undefined
1050
+ ? undefined
1051
+ : { columnName: relation.columnName, field: inner.field };
1052
+ }
1053
+ // ---------------------------------------------------------------------------
1054
+ // Tables
1055
+ // ---------------------------------------------------------------------------
1056
+ function columnNameOf(model, name) {
1057
+ const relation = model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.name === name) ??
1058
+ model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.columnName === name);
1059
+ const field = model.fields.find((candidate) => candidate.name === name) ??
1060
+ model.fields.find((candidate) => candidate.columnName === name);
1061
+ // A field wins over a relation of the same name: the field is the column the key is written on.
1062
+ return field?.columnName ?? relation?.columnName;
1063
+ }
1064
+ function isAutoIncrement(field) {
1065
+ return field.default?.kind === 'autoIncrement';
1066
+ }
1067
+ function buildTable(context, model) {
1068
+ const dialect = context.dialect;
1069
+ checkIdentifierLength(context, model.name, model.tableName);
1070
+ const columns = [];
1071
+ const known = new Set();
1072
+ // The primary key: the composite key when there is one, otherwise the key fields and relations.
1073
+ const composite = (model.compositePrimaryKey ?? [])
1074
+ .map((name) => columnNameOf(model, name))
1075
+ .filter((name) => name !== undefined);
1076
+ const declaredKey = [
1077
+ ...model.fields
1078
+ .filter((field) => field.isPrimaryKey)
1079
+ .map((field) => field.columnName),
1080
+ ...model.relations
1081
+ .filter((relation) => relation.kind !== 'manyToMany' && relation.isPrimaryKey === true)
1082
+ .map((relation) => relation.columnName),
1083
+ ];
1084
+ const primaryKey = composite.length > 0 ? composite : declaredKey;
1085
+ const singleColumnKey = primaryKey.length === 1;
1086
+ for (const field of model.fields) {
1087
+ checkIdentifierLength(context, `${model.name}.${field.name}`, field.columnName);
1088
+ known.add(field.columnName);
1089
+ columns.push(fieldColumn(context, model, field, primaryKey, singleColumnKey));
1090
+ }
1091
+ const foreignKeys = [];
1092
+ const uniques = [];
1093
+ for (const relation of model.relations) {
1094
+ if (relation.kind === 'manyToMany') {
1095
+ continue;
1096
+ }
1097
+ const label = `${model.name}.${relation.name}`;
1098
+ const target = findModel(context.schema, relation.targetModel);
1099
+ if (target === undefined) {
1100
+ context.warnings.push(`${label}: target model "${relation.targetModel}" does not exist in the schema; the relation was skipped.`);
1101
+ continue;
1102
+ }
1103
+ if (target.isView === true) {
1104
+ context.warnings.push(`${label}: target "${target.name}" is a view, which a foreign key cannot reference; the relation was skipped.`);
1105
+ continue;
1106
+ }
1107
+ const referenced = resolveTarget(context, target, relation.toField);
1108
+ if (referenced === undefined) {
1109
+ context.warnings.push(`${label}: target model "${target.name}" has no single column to reference (a composite or missing key); the relation was skipped.`);
1110
+ continue;
1111
+ }
1112
+ warnAboutUnreferenceable(context, label, target, referenced);
1113
+ if (!known.has(relation.columnName)) {
1114
+ checkIdentifierLength(context, label, relation.columnName);
1115
+ known.add(relation.columnName);
1116
+ columns.push(relationColumn(context, model, relation, referenced, primaryKey, singleColumnKey));
1117
+ }
1118
+ const keyed = relation.isPrimaryKey === true ||
1119
+ primaryKey.includes(relation.columnName);
1120
+ if (relation.kind === 'oneToOne' && !keyed) {
1121
+ uniques.push({
1122
+ name: generatedName(context, model.tableName, [relation.columnName], 'key'),
1123
+ columns: [relation.columnName],
1124
+ });
1125
+ }
1126
+ if (relation.onDelete === 'setNull' && !relation.isNullable) {
1127
+ context.warnings.push(`${label}: onDelete SET NULL on a required relation will fail at the database level; review the relation.`);
1128
+ }
1129
+ foreignKeys.push({
1130
+ name: claimName(context, relation.constraintName ??
1131
+ limitIdentifier(`${model.tableName}_${relation.columnName}_fkey`, MAX_IDENTIFIER[dialect])),
1132
+ columns: [relation.columnName],
1133
+ target,
1134
+ targetColumns: [referenced.columnName],
1135
+ onDelete: relation.onDelete,
1136
+ onUpdate: relation.onUpdate,
1137
+ label,
1138
+ deferred: false,
1139
+ });
1140
+ }
1141
+ for (const key of model.compositeForeignKeys ?? []) {
1142
+ const built = compositeKey(context, model, key);
1143
+ if (built !== undefined) {
1144
+ foreignKeys.push(built);
1145
+ }
1146
+ }
1147
+ for (const field of model.fields) {
1148
+ if (field.isUnique &&
1149
+ !field.isPrimaryKey &&
1150
+ !primaryKey.includes(field.columnName)) {
1151
+ uniques.push({
1152
+ name: claimName(context, field.uniqueName ??
1153
+ limitIdentifier(`${model.tableName}_${field.columnName}_key`, MAX_IDENTIFIER[dialect])),
1154
+ columns: [field.columnName],
1155
+ });
1156
+ }
1157
+ }
1158
+ const inline = dialect === 'sqlite' &&
1159
+ singleColumnKey &&
1160
+ columns.some((column) => column.autoIncrement && column.name === primaryKey[0]);
1161
+ return {
1162
+ model,
1163
+ columns,
1164
+ primaryKey,
1165
+ primaryKeyName: claimName(context, model.primaryKeyName ??
1166
+ limitIdentifier(`${model.tableName}_pkey`, MAX_IDENTIFIER[dialect])),
1167
+ inlinePrimaryKey: inline,
1168
+ uniques,
1169
+ foreignKeys,
1170
+ indexes: [],
1171
+ };
1172
+ }
1173
+ function warnAboutUnreferenceable(context, label, target, referenced) {
1174
+ const field = referenced.field;
1175
+ const isKey = field.isPrimaryKey ||
1176
+ field.isUnique ||
1177
+ target.relations.some((candidate) => candidate.isPrimaryKey === true &&
1178
+ candidate.columnName === referenced.columnName) ||
1179
+ target.indexes.some((index) => index.isUnique &&
1180
+ index.fields.length === 1 &&
1181
+ columnNameOf(target, index.fields[0] ?? '') === referenced.columnName) ||
1182
+ (target.compositePrimaryKey?.length === 1 &&
1183
+ columnNameOf(target, target.compositePrimaryKey[0] ?? '') ===
1184
+ referenced.columnName);
1185
+ if (!isKey) {
1186
+ context.warnings.push(`${label}: the referenced column ${target.tableName}.${referenced.columnName} is not a primary key or unique; the database may reject the foreign key.`);
1187
+ }
1188
+ }
1189
+ function compositeKey(context, model, key) {
1190
+ const label = `${model.name}.${key.name}`;
1191
+ const target = findModel(context.schema, key.targetModel);
1192
+ if (target === undefined || target.isView === true) {
1193
+ context.warnings.push(`${label}: target model "${key.targetModel}" is not a table in the schema; the composite foreign key was skipped.`);
1194
+ return undefined;
1195
+ }
1196
+ const columns = key.fields.map((name) => columnNameOf(model, name) ?? name);
1197
+ const targetColumns = key.references.map((name) => columnNameOf(target, name) ?? name);
1198
+ const missing = [
1199
+ ...key.fields.filter((name) => columnNameOf(model, name) === undefined),
1200
+ ...key.references.filter((name) => columnNameOf(target, name) === undefined),
1201
+ ][0];
1202
+ if (missing !== undefined || columns.length !== targetColumns.length) {
1203
+ context.warnings.push(`${label}: the composite foreign key refers to "${missing ?? ''}", which is not a field of the model; it was skipped.`);
1204
+ return undefined;
1205
+ }
1206
+ return {
1207
+ name: claimName(context, key.constraintName ??
1208
+ limitIdentifier(`${model.tableName}_${columns.join('_')}_fkey`, MAX_IDENTIFIER[context.dialect])),
1209
+ columns,
1210
+ target,
1211
+ targetColumns,
1212
+ onDelete: key.onDelete,
1213
+ onUpdate: key.onUpdate,
1214
+ label,
1215
+ deferred: false,
1216
+ };
1217
+ }
1218
+ /** Builds the definition of a column from a field (or from a field-shaped description of a relation). */
1219
+ function buildColumn(context, label, field, columnName, inKey, singleColumnKey) {
1220
+ const dialect = context.dialect;
1221
+ const columnSql = quote(context, columnName);
1222
+ const type = columnType(context, label, field, columnSql);
1223
+ const notes = type.comment === undefined ? [] : [type.comment];
1224
+ if (field.generated !== undefined) {
1225
+ context.warnings.push(`${label}: the generated column expression ${field.generated.expression} is not SQL; the column was written as a regular column.`);
1226
+ notes.push('generated column written as a regular column');
1227
+ }
1228
+ let autoIncrement = false;
1229
+ if (isAutoIncrement(field)) {
1230
+ if (!isIntegerKey(field)) {
1231
+ context.warnings.push(`${label}: an auto-increment default on a ${field.type} column cannot be written; it was dropped.`);
1232
+ }
1233
+ else if (dialect === 'sqlite' && !(inKey && singleColumnKey)) {
1234
+ context.warnings.push(`${label}: SQLite can auto-increment only a single-column integer primary key; the auto-increment was dropped.`);
1235
+ }
1236
+ else if ((field.arrayDepth ?? 0) > 0) {
1237
+ context.warnings.push(`${label}: an array column cannot auto-increment; it was dropped.`);
1238
+ }
1239
+ else {
1240
+ autoIncrement = true;
1241
+ }
1242
+ }
1243
+ let sqlType = type.sql;
1244
+ if (autoIncrement && dialect === 'sqlite') {
1245
+ // Only INTEGER PRIMARY KEY is the rowid and may AUTOINCREMENT.
1246
+ sqlType = 'INTEGER';
1247
+ }
1248
+ let defaultText = autoIncrement
1249
+ ? undefined
1250
+ : defaultSql(context, label, field, type);
1251
+ let onUpdate;
1252
+ if (field.isAutoUpdated) {
1253
+ if (dialect === 'mysql' && field.type === 'dateTime') {
1254
+ onUpdate = mysqlCurrentTimestamp(type.sql);
1255
+ defaultText ??= onUpdate;
1256
+ }
1257
+ else {
1258
+ context.unexpressedAutoUpdates.push(label);
1259
+ }
1260
+ }
1261
+ const nullable = field.isNullable && !inKey;
1262
+ return {
1263
+ name: columnName,
1264
+ sqlType,
1265
+ notNull: !nullable,
1266
+ ...(defaultText === undefined ? {} : { defaultSql: defaultText }),
1267
+ autoIncrement,
1268
+ ...(onUpdate === undefined ? {} : { onUpdate }),
1269
+ checks: type.checks,
1270
+ ...(notes.length === 0 ? {} : { comment: notes.join('; ') }),
1271
+ storage: type.storage,
1272
+ unindexable: type.unindexable,
1273
+ };
1274
+ }
1275
+ function fieldColumn(context, model, field, primaryKey, singleColumnKey) {
1276
+ return buildColumn(context, `${model.name}.${field.name}`, field, field.columnName, primaryKey.includes(field.columnName), singleColumnKey);
1277
+ }
1278
+ function relationColumn(context, model, relation, referenced, primaryKey, singleColumnKey) {
1279
+ const asField = {
1280
+ ...referenced.field,
1281
+ name: relation.name,
1282
+ columnName: relation.columnName,
1283
+ isPrimaryKey: false,
1284
+ isUnique: false,
1285
+ isNullable: relation.isNullable,
1286
+ isAutoUpdated: false,
1287
+ default: undefined,
1288
+ generated: undefined,
1289
+ };
1290
+ return buildColumn(context, `${model.name}.${relation.name}`, asField, relation.columnName, primaryKey.includes(relation.columnName), singleColumnKey);
1291
+ }
1292
+ // ---------------------------------------------------------------------------
1293
+ // Ordering
1294
+ // ---------------------------------------------------------------------------
1295
+ /**
1296
+ * Orders the tables so that a table comes after the tables its foreign keys point at. A key that
1297
+ * closes a cycle is marked deferred (written with ALTER TABLE after every table exists); a
1298
+ * reference to the table itself stays inside CREATE TABLE.
1299
+ */
1300
+ function orderTables(context, tables) {
1301
+ const byModel = new Map();
1302
+ for (const table of tables) {
1303
+ byModel.set(table.model.name, table);
1304
+ }
1305
+ const ordered = [];
1306
+ const done = new Set();
1307
+ const visiting = new Set();
1308
+ const visit = (table) => {
1309
+ if (done.has(table.model.name)) {
1310
+ return;
1311
+ }
1312
+ visiting.add(table.model.name);
1313
+ for (const key of table.foreignKeys) {
1314
+ const target = byModel.get(key.target.name);
1315
+ if (target === undefined || target === table) {
1316
+ continue;
1317
+ }
1318
+ if (visiting.has(target.model.name)) {
1319
+ key.deferred = true;
1320
+ }
1321
+ else {
1322
+ visit(target);
1323
+ }
1324
+ }
1325
+ visiting.delete(table.model.name);
1326
+ done.add(table.model.name);
1327
+ ordered.push(table);
1328
+ };
1329
+ for (const table of tables) {
1330
+ visit(table);
1331
+ }
1332
+ if (context.dialect === 'sqlite' &&
1333
+ ordered.some((table) => table.foreignKeys.some((key) => key.deferred))) {
1334
+ // SQLite cannot add a constraint to an existing table, but accepts a reference to a table that
1335
+ // does not exist yet, so the keys stay inside CREATE TABLE.
1336
+ for (const table of ordered) {
1337
+ for (const key of table.foreignKeys) {
1338
+ key.deferred = false;
1339
+ }
1340
+ }
1341
+ context.warnings.push('Some tables reference each other in a cycle; SQLite cannot add a foreign key afterwards, so each key stays in its CREATE TABLE and refers to a table created later (valid in SQLite).');
1342
+ }
1343
+ return ordered;
1344
+ }
1345
+ /**
1346
+ * SQL Server rejects a foreign key whose cascading action would create a cycle or a second cascade
1347
+ * path to a table. Those keys are written with NO ACTION instead, and the warning names them.
1348
+ */
1349
+ function limitCascadePaths(context, ordered) {
1350
+ const edges = new Map();
1351
+ const reachFrom = (start) => {
1352
+ const seen = new Set();
1353
+ const queue = [start];
1354
+ while (queue.length > 0) {
1355
+ const current = queue.pop();
1356
+ for (const next of edges.get(current ?? '') ?? []) {
1357
+ if (!seen.has(next)) {
1358
+ seen.add(next);
1359
+ queue.push(next);
1360
+ }
1361
+ }
1362
+ }
1363
+ return seen;
1364
+ };
1365
+ const cascades = (action) => action === 'cascade' || action === 'setNull' || action === 'setDefault';
1366
+ for (const table of ordered) {
1367
+ for (const key of table.foreignKeys) {
1368
+ if (!cascades(key.onDelete) && !cascades(key.onUpdate)) {
1369
+ continue;
1370
+ }
1371
+ const parent = key.target.name;
1372
+ const child = table.model.name;
1373
+ let conflict = parent === child || reachFrom(child).has(parent);
1374
+ if (!conflict) {
1375
+ // A second path: some table that already reaches the parent (or is the parent) reaches the
1376
+ // child, or anything below it, another way.
1377
+ const below = reachFrom(child);
1378
+ below.add(child);
1379
+ for (const root of edges.keys()) {
1380
+ if (root !== parent && !reachFrom(root).has(parent)) {
1381
+ continue;
1382
+ }
1383
+ const reached = reachFrom(root);
1384
+ if ([...below].some((name) => reached.has(name))) {
1385
+ conflict = true;
1386
+ break;
1387
+ }
1388
+ }
1389
+ }
1390
+ if (conflict) {
1391
+ context.warnings.push(`${key.label}: SQL Server rejects cascading actions that form a cycle or a second cascade path; the foreign key was written with NO ACTION.`);
1392
+ key.onDelete = 'noAction';
1393
+ key.onUpdate = undefined;
1394
+ continue;
1395
+ }
1396
+ const children = edges.get(parent) ?? new Set();
1397
+ children.add(child);
1398
+ edges.set(parent, children);
1399
+ }
1400
+ }
1401
+ }
1402
+ // ---------------------------------------------------------------------------
1403
+ // Indexes
1404
+ // ---------------------------------------------------------------------------
1405
+ function referentialAction(context, label, kind, action) {
1406
+ if (action === undefined || action === 'noAction') {
1407
+ return undefined;
1408
+ }
1409
+ if (action === 'setDefault' && context.dialect === 'mysql') {
1410
+ context.warnings.push(`${label}: MySQL (InnoDB) does not support ${kind} SET DEFAULT; the action was left at the default (NO ACTION).`);
1411
+ return undefined;
1412
+ }
1413
+ switch (action) {
1414
+ case 'cascade':
1415
+ return `${kind} CASCADE`;
1416
+ case 'setNull':
1417
+ return `${kind} SET NULL`;
1418
+ case 'setDefault':
1419
+ return `${kind} SET DEFAULT`;
1420
+ default:
1421
+ // SQL Server has no RESTRICT; NO ACTION behaves the same way there.
1422
+ return context.dialect === 'sqlserver' ? undefined : `${kind} RESTRICT`;
1423
+ }
1424
+ }
1425
+ function indexColumnSql(context, table, label, column, options) {
1426
+ const def = table.columns.find((candidate) => candidate.name === column);
1427
+ if (def === undefined) {
1428
+ return undefined;
1429
+ }
1430
+ let text = quote(context, column);
1431
+ if (context.dialect === 'mysql') {
1432
+ let prefix = options?.length;
1433
+ if (prefix === undefined &&
1434
+ (def.storage === 'text' || def.storage === 'blob')) {
1435
+ prefix = 255;
1436
+ context.warnings.push(`${label}: MySQL cannot index all of a TEXT or BLOB column; the key covers only its first 255 ${def.storage === 'text' ? 'characters' : 'bytes'}.`);
1437
+ }
1438
+ if (prefix !== undefined) {
1439
+ text += `(${prefix})`;
1440
+ }
1441
+ }
1442
+ else if (options?.length !== undefined) {
1443
+ context.warnings.push(`${label}: the prefix length of "${column}" is only supported by MySQL and was dropped.`);
1444
+ }
1445
+ if (context.dialect === 'postgresql' && options?.ops !== undefined) {
1446
+ const raw = /^raw\(\s*["'](.*)["']\s*\)$/.exec(options.ops);
1447
+ text += ` ${raw?.[1] ?? toSnakeCase(options.ops)}`;
1448
+ }
1449
+ else if (options?.ops !== undefined) {
1450
+ context.warnings.push(`${label}: the operator class of "${column}" is only supported by PostgreSQL and was dropped.`);
1451
+ }
1452
+ if (options?.sort === 'desc') {
1453
+ text += ' DESC';
1454
+ }
1455
+ return text;
1456
+ }
1457
+ function buildIndexes(context, table) {
1458
+ const model = table.model;
1459
+ const dialect = context.dialect;
1460
+ for (const index of model.indexes) {
1461
+ const label = `${model.name} index (${index.fields.join(', ')})`;
1462
+ const columns = [];
1463
+ let complete = true;
1464
+ for (const name of index.fields) {
1465
+ const column = columnNameOf(model, name);
1466
+ if (column === undefined) {
1467
+ context.warnings.push(`${model.name}: an index references "${name}", which is not a field of the model; the index was skipped.`);
1468
+ complete = false;
1469
+ break;
1470
+ }
1471
+ columns.push(column);
1472
+ }
1473
+ if (!complete || columns.length === 0) {
1474
+ continue;
1475
+ }
1476
+ const blocked = columns.find((column) => table.columns.find((candidate) => candidate.name === column)
1477
+ ?.unindexable === true);
1478
+ if (blocked !== undefined) {
1479
+ context.warnings.push(`${label}: ${DIALECT_LABELS[dialect]} cannot index the column "${blocked}" (its type is unbounded or JSON); the index was skipped.`);
1480
+ continue;
1481
+ }
1482
+ const name = claimName(context, index.name ??
1483
+ limitIdentifier(`${model.tableName}_${columns.join('_')}_${index.isUnique ? 'key' : 'idx'}`, MAX_IDENTIFIER[dialect]));
1484
+ const entries = [];
1485
+ for (let position = 0; position < columns.length; position += 1) {
1486
+ const column = columns[position] ?? '';
1487
+ const options = index.fieldOptions?.[index.fields[position] ?? ''] ?? undefined;
1488
+ const text = indexColumnSql(context, table, label, column, options);
1489
+ if (text !== undefined) {
1490
+ entries.push(text);
1491
+ }
1492
+ }
1493
+ table.indexes.push({
1494
+ statement: indexStatement(context, table, label, index, name, columns, entries),
1495
+ });
1496
+ }
1497
+ // A unique key on a column the database cannot index would fail the whole script; leave it out.
1498
+ table.uniques = table.uniques.filter((unique) => {
1499
+ const blocked = unique.columns.find((column) => table.columns.find((candidate) => candidate.name === column)
1500
+ ?.unindexable === true);
1501
+ if (blocked !== undefined) {
1502
+ context.warnings.push(`${model.name}.${blocked}: ${DIALECT_LABELS[dialect]} cannot index the column "${blocked}" (its type is unbounded or JSON); the unique constraint was skipped.`);
1503
+ }
1504
+ return blocked === undefined;
1505
+ });
1506
+ // A unique column on SQL Server allows only one NULL; a filtered unique index keeps the other
1507
+ // dialects' meaning (any number of NULLs).
1508
+ if (dialect === 'sqlserver') {
1509
+ const remaining = [];
1510
+ for (const unique of table.uniques) {
1511
+ const nullable = unique.columns.filter((column) => table.columns.some((candidate) => candidate.name === column && !candidate.notNull));
1512
+ if (nullable.length === 0) {
1513
+ remaining.push(unique);
1514
+ continue;
1515
+ }
1516
+ table.indexes.push({
1517
+ statement: `CREATE UNIQUE INDEX ${quote(context, unique.name)} ON ${quote(context, model.tableName)} (${quoteList(context, unique.columns)}) WHERE ${nullable
1518
+ .map((column) => `${quote(context, column)} IS NOT NULL`)
1519
+ .join(' AND ')};`,
1520
+ });
1521
+ }
1522
+ table.uniques = remaining;
1523
+ }
1524
+ }
1525
+ function indexStatement(context, table, label, index, name, columns, entries) {
1526
+ const dialect = context.dialect;
1527
+ const tableSql = quote(context, table.model.tableName);
1528
+ const nameSql = quote(context, name);
1529
+ const list = entries.join(', ');
1530
+ if (index.kind === 'fulltext') {
1531
+ if (dialect === 'mysql') {
1532
+ return `CREATE FULLTEXT INDEX ${nameSql} ON ${tableSql} (${list});`;
1533
+ }
1534
+ if (dialect === 'postgresql') {
1535
+ const vector = columns
1536
+ .map((column) => `coalesce(${quote(context, column)}::text, '')`)
1537
+ .join(" || ' ' || ");
1538
+ return `CREATE INDEX ${nameSql} ON ${tableSql} USING gin (to_tsvector('simple', ${vector}));`;
1539
+ }
1540
+ context.warnings.push(`${label}: ${DIALECT_LABELS[dialect]} full-text search needs a separate mechanism (a virtual table or a catalog); it was written as an ordinary index.`);
1541
+ }
1542
+ const parts = ['CREATE'];
1543
+ if (index.isUnique) {
1544
+ parts.push('UNIQUE');
1545
+ }
1546
+ if (index.clustered !== undefined) {
1547
+ if (dialect === 'sqlserver') {
1548
+ parts.push(index.clustered ? 'CLUSTERED' : 'NONCLUSTERED');
1549
+ }
1550
+ else {
1551
+ context.warnings.push(`${label}: clustered indexes exist only on SQL Server; the option was dropped.`);
1552
+ }
1553
+ }
1554
+ let target = tableSql;
1555
+ let trailing = '';
1556
+ if (index.method !== undefined) {
1557
+ const method = index.method.toUpperCase();
1558
+ if (dialect === 'postgresql') {
1559
+ target = `${tableSql} USING ${method.toLowerCase()}`;
1560
+ }
1561
+ else if (dialect === 'mysql' &&
1562
+ (method === 'BTREE' || method === 'HASH')) {
1563
+ trailing = ` USING ${method}`;
1564
+ }
1565
+ else if (method !== 'BTREE') {
1566
+ // B-tree is what every database builds anyway.
1567
+ context.warnings.push(`${label}: the index type ${index.method} is not supported by ${DIALECT_LABELS[dialect]} and was dropped.`);
1568
+ }
1569
+ }
1570
+ parts.push('INDEX', nameSql, 'ON', target);
1571
+ const nullableColumns = dialect === 'sqlserver' && index.isUnique
1572
+ ? columns.filter((column) => table.columns.some((candidate) => candidate.name === column && !candidate.notNull))
1573
+ : [];
1574
+ const filter = nullableColumns.length === 0
1575
+ ? ''
1576
+ : ` WHERE ${nullableColumns.map((column) => `${quote(context, column)} IS NOT NULL`).join(' AND ')}`;
1577
+ return `${parts.join(' ')} (${list})${trailing}${filter};`;
1578
+ }
1579
+ // ---------------------------------------------------------------------------
1580
+ // Rendering
1581
+ // ---------------------------------------------------------------------------
1582
+ function columnLine(context, table, column) {
1583
+ const dialect = context.dialect;
1584
+ const parts = [quote(context, column.name), column.sqlType];
1585
+ if (column.autoIncrement) {
1586
+ switch (dialect) {
1587
+ case 'postgresql':
1588
+ parts.push('GENERATED BY DEFAULT AS IDENTITY');
1589
+ break;
1590
+ case 'sqlserver':
1591
+ parts.push('IDENTITY(1,1)');
1592
+ break;
1593
+ default:
1594
+ break;
1595
+ }
1596
+ }
1597
+ if (table.inlinePrimaryKey && column.autoIncrement) {
1598
+ parts.push('PRIMARY KEY AUTOINCREMENT');
1599
+ return parts.join(' ');
1600
+ }
1601
+ if (column.notNull) {
1602
+ parts.push('NOT NULL');
1603
+ }
1604
+ else if (dialect === 'sqlserver') {
1605
+ // SQL Server's default nullability depends on session settings, so say it.
1606
+ parts.push('NULL');
1607
+ }
1608
+ if (column.autoIncrement && dialect === 'mysql') {
1609
+ parts.push('AUTO_INCREMENT');
1610
+ }
1611
+ if (column.defaultSql !== undefined) {
1612
+ parts.push(`DEFAULT ${column.defaultSql}`);
1613
+ }
1614
+ if (column.onUpdate !== undefined) {
1615
+ parts.push(`ON UPDATE ${column.onUpdate}`);
1616
+ }
1617
+ return parts.join(' ');
1618
+ }
1619
+ function foreignKeyClause(context, key) {
1620
+ const actions = [
1621
+ referentialAction(context, key.label, 'ON DELETE', key.onDelete),
1622
+ referentialAction(context, key.label, 'ON UPDATE', key.onUpdate),
1623
+ ].filter((action) => action !== undefined);
1624
+ return (`CONSTRAINT ${quote(context, key.name)} FOREIGN KEY (${quoteList(context, key.columns)}) ` +
1625
+ `REFERENCES ${quote(context, key.target.tableName)} (${quoteList(context, key.targetColumns)})` +
1626
+ (actions.length === 0 ? '' : ` ${actions.join(' ')}`));
1627
+ }
1628
+ function renderTable(context, table) {
1629
+ const dialect = context.dialect;
1630
+ const items = [];
1631
+ for (const column of table.columns) {
1632
+ items.push({
1633
+ sql: columnLine(context, table, column),
1634
+ ...(column.comment === undefined ? {} : { comment: column.comment }),
1635
+ });
1636
+ }
1637
+ if (table.primaryKey.length > 0 && !table.inlinePrimaryKey) {
1638
+ items.push({
1639
+ sql: `CONSTRAINT ${quote(context, table.primaryKeyName)} PRIMARY KEY (${quoteList(context, table.primaryKey)})`,
1640
+ });
1641
+ }
1642
+ for (const unique of table.uniques) {
1643
+ const columns = unique.columns
1644
+ .map((name) => {
1645
+ const def = table.columns.find((candidate) => candidate.name === name);
1646
+ const base = quote(context, name);
1647
+ if (dialect === 'mysql' &&
1648
+ def !== undefined &&
1649
+ (def.storage === 'text' || def.storage === 'blob')) {
1650
+ context.warnings.push(`${table.model.name}.${name}: MySQL cannot make all of a TEXT or BLOB column unique; the constraint covers only its first 255 ${def.storage === 'text' ? 'characters' : 'bytes'}.`);
1651
+ return `${base}(255)`;
1652
+ }
1653
+ return base;
1654
+ })
1655
+ .join(', ');
1656
+ items.push({
1657
+ sql: `CONSTRAINT ${quote(context, unique.name)} UNIQUE (${columns})`,
1658
+ });
1659
+ }
1660
+ for (const column of table.columns) {
1661
+ for (const check of column.checks) {
1662
+ const name = claimName(context, limitIdentifier(`${table.model.tableName}_${column.name}_check`, MAX_IDENTIFIER[dialect]));
1663
+ items.push({
1664
+ sql: `CONSTRAINT ${quote(context, name)} CHECK (${check})`,
1665
+ });
1666
+ }
1667
+ }
1668
+ for (const key of table.foreignKeys) {
1669
+ if (!key.deferred) {
1670
+ items.push({ sql: foreignKeyClause(context, key) });
1671
+ }
1672
+ }
1673
+ const lines = [];
1674
+ items.forEach((item, position) => {
1675
+ const comma = position === items.length - 1 ? '' : ',';
1676
+ lines.push(` ${item.sql}${comma}${item.comment === undefined ? '' : ` -- ${commentText(item.comment)}`}`);
1677
+ });
1678
+ const tail = dialect === 'mysql' ? ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' : '';
1679
+ return [
1680
+ `CREATE TABLE ${quote(context, table.model.tableName)} (`,
1681
+ ...lines,
1682
+ `)${tail};`,
1683
+ ].join('\n');
1684
+ }
1685
+ function renderView(context, view) {
1686
+ const columns = [
1687
+ ...view.fields.map((field) => field.columnName),
1688
+ ...view.relations
1689
+ .filter((relation) => relation.kind !== 'manyToMany')
1690
+ .map((relation) => relation.columnName),
1691
+ ].join(', ');
1692
+ return [
1693
+ `-- View ${quote(context, view.tableName)}: the schema has no SQL definition for it, so no statement was written.`,
1694
+ `-- Columns: ${commentText(columns)}`,
1695
+ `-- CREATE VIEW ${quote(context, view.tableName)} AS SELECT ... ;`,
1696
+ ].join('\n');
1697
+ }
1698
+ function render(context, ordered, views) {
1699
+ const dialect = context.dialect;
1700
+ const blocks = [
1701
+ `-- SQL DDL for ${DIALECT_LABELS[dialect]}, generated by ormbridge.`,
1702
+ ];
1703
+ if (dialect === 'sqlite') {
1704
+ blocks.push('PRAGMA foreign_keys = ON;');
1705
+ }
1706
+ const tableTexts = ordered.map((table) => renderTable(context, table));
1707
+ if (dialect === 'postgresql') {
1708
+ if (context.needsHstore) {
1709
+ blocks.push('CREATE EXTENSION IF NOT EXISTS hstore;');
1710
+ }
1711
+ for (const info of context.enums.values()) {
1712
+ blocks.push(`CREATE TYPE ${quote(context, info.dbName)} AS ENUM (${info.definition.values
1713
+ .map((value) => sqlStringLiteral(dialect, value.dbValue))
1714
+ .join(', ')});`);
1715
+ }
1716
+ }
1717
+ ordered.forEach((table, position) => {
1718
+ const text = tableTexts[position] ?? '';
1719
+ const indexes = table.indexes.length === 0
1720
+ ? ''
1721
+ : `\n${table.indexes.map((index) => index.statement).join('\n')}`;
1722
+ blocks.push(`${text}${indexes}`);
1723
+ });
1724
+ for (const view of views) {
1725
+ blocks.push(renderView(context, view));
1726
+ }
1727
+ const deferred = [];
1728
+ for (const table of ordered) {
1729
+ for (const key of table.foreignKeys) {
1730
+ if (key.deferred) {
1731
+ deferred.push(`ALTER TABLE ${quote(context, table.model.tableName)} ADD ${foreignKeyClause(context, key)};`);
1732
+ }
1733
+ }
1734
+ }
1735
+ if (deferred.length > 0) {
1736
+ blocks.push('-- These foreign keys close a reference cycle, so they are added once every table exists.');
1737
+ blocks.push(deferred.join('\n'));
1738
+ }
1739
+ return `${blocks.join('\n\n')}\n`;
1740
+ }
1741
+ //# sourceMappingURL=sqlDdl.js.map