@an5/orm 1.0.8 → 1.1.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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -0,0 +1,731 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSqlType = parseSqlType;
4
+ exports.formatDbColumnSqlType = formatDbColumnSqlType;
5
+ exports.mapDefault = mapDefault;
6
+ exports.safeIdentifierName = safeIdentifierName;
7
+ exports.quoteTableName = quoteTableName;
8
+ exports.tableIdentityName = tableIdentityName;
9
+ exports.buildAlterColumnWarnings = buildAlterColumnWarnings;
10
+ exports.buildAlterColumnPreflightSql = buildAlterColumnPreflightSql;
11
+ exports.buildAddColumnPreflightSql = buildAddColumnPreflightSql;
12
+ exports.buildUniqueConstraintPreflightSql = buildUniqueConstraintPreflightSql;
13
+ exports.parseSchemaText = parseSchemaText;
14
+ exports.buildCreateTableSql = buildCreateTableSql;
15
+ exports.buildIndexDiff = buildIndexDiff;
16
+ exports.generateColumnDiff = generateColumnDiff;
17
+ exports.generateDiff = generateDiff;
18
+ exports.buildMigrationFile = buildMigrationFile;
19
+ exports.buildDownMigrationSql = buildDownMigrationSql;
20
+ exports.parseMigrationSections = parseMigrationSections;
21
+ exports.splitSqlBatches = splitSqlBatches;
22
+ exports.parseRollbackSelection = parseRollbackSelection;
23
+ exports.parseMigrationCommandOptions = parseMigrationCommandOptions;
24
+ const field_types_1 = require("./generator/src/field-types");
25
+ function parseSqlType(raw) {
26
+ const match = raw.match(/^(\w+)/);
27
+ return match?.[1]?.toUpperCase() ?? raw.toUpperCase();
28
+ }
29
+ function formatDbColumnSqlType(column) {
30
+ const base = parseSqlType(column.dataType || '');
31
+ if ((base === 'NVARCHAR' || base === 'NCHAR') && typeof column.maxLength === 'number') {
32
+ return `${base}(${column.maxLength < 0 ? 'MAX' : Math.floor(column.maxLength / 2)})`;
33
+ }
34
+ if ((base === 'VARCHAR' || base === 'CHAR' || base === 'VARBINARY' || base === 'BINARY') && typeof column.maxLength === 'number') {
35
+ return `${base}(${column.maxLength < 0 ? 'MAX' : column.maxLength})`;
36
+ }
37
+ if ((base === 'DECIMAL' || base === 'NUMERIC') && typeof column.precision === 'number' && typeof column.scale === 'number') {
38
+ return `${base}(${column.precision},${column.scale})`;
39
+ }
40
+ if ((base === 'DATETIME2' || base === 'TIME' || base === 'DATETIMEOFFSET') && typeof column.scale === 'number') {
41
+ return `${base}(${column.scale})`;
42
+ }
43
+ return base;
44
+ }
45
+ function mapDefault(val) {
46
+ if (val === 'uuid()')
47
+ return 'DEFAULT NEWID()';
48
+ if (val === 'cuid()')
49
+ return 'DEFAULT NEWID()';
50
+ if (val === 'now()')
51
+ return 'DEFAULT CURRENT_TIMESTAMP';
52
+ if (val === 'autoincrement()')
53
+ return 'IDENTITY(1,1)';
54
+ if (val === 'true')
55
+ return 'DEFAULT 1';
56
+ if (val === 'false')
57
+ return 'DEFAULT 0';
58
+ if (/^".*"$/.test(val))
59
+ return `DEFAULT '${val.slice(1, -1).replace(/'/g, "''")}'`;
60
+ return `DEFAULT ${val}`;
61
+ }
62
+ function safeIdentifierName(raw) {
63
+ return raw.replace(/[^A-Za-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
64
+ }
65
+ function fieldUniqueConstraintName(model, field) {
66
+ return `UQ_${safeIdentifierName(model.tableName)}_${safeIdentifierName(field.name)}`;
67
+ }
68
+ function compoundUniqueConstraintName(model, idx) {
69
+ return `UQ_${safeIdentifierName(model.tableName)}_compound_${idx}`;
70
+ }
71
+ function indexName(model, fields) {
72
+ return `IX_${safeIdentifierName(model.tableName)}_${safeIdentifierName(fields.join('_'))}`;
73
+ }
74
+ function schemaIndexFields(definition) {
75
+ return Array.isArray(definition) ? definition : definition.fields;
76
+ }
77
+ function schemaIndexName(definition, fallback) {
78
+ if (!Array.isArray(definition) && definition.name)
79
+ return definition.name;
80
+ return fallback(schemaIndexFields(definition));
81
+ }
82
+ function parseMappedIndex(line, directive) {
83
+ const match = line.match(new RegExp(`${directive}\\(\\[([\\w,\\s]+)\\]([^)]*)\\)`));
84
+ if (!match)
85
+ return null;
86
+ const fields = (match[1] ?? '').split(',').map(field => field.trim()).filter(Boolean);
87
+ const nameMatch = match[2]?.match(/\bmap\s*:\s*"([^"]+)"/);
88
+ const includeMatch = match[2]?.match(/\binclude\s*:\s*\[([\w,\s]+)\]/);
89
+ const filterMatch = match[2]?.match(/\bfilter\s*:\s*"([^"]+)"/);
90
+ const optionsMatch = match[2]?.match(/\boptions\s*:\s*"([^"]+)"/);
91
+ const definition = { fields };
92
+ if (nameMatch?.[1])
93
+ definition.name = nameMatch[1];
94
+ if (includeMatch?.[1])
95
+ definition.includeFields = includeMatch[1].split(',').map(field => field.trim()).filter(Boolean);
96
+ if (filterMatch?.[1])
97
+ definition.filter = filterMatch[1];
98
+ if (optionsMatch?.[1])
99
+ definition.options = optionsMatch[1];
100
+ return definition;
101
+ }
102
+ function quoteTableName(raw) {
103
+ return raw
104
+ .replace(/"/g, '')
105
+ .split('.')
106
+ .map(part => part.trim().replace(/^\[|\]$/g, ''))
107
+ .filter(Boolean)
108
+ .map(part => `[${part.replace(/]/g, ']]')}]`)
109
+ .join('.');
110
+ }
111
+ function tableIdentityName(raw) {
112
+ const cleaned = raw
113
+ .replace(/"/g, '')
114
+ .split('.')
115
+ .map(part => part.trim().replace(/^\[|\]$/g, '').toLowerCase())
116
+ .filter(Boolean);
117
+ if (cleaned.length > 1 && cleaned[0] === 'dbo') {
118
+ cleaned.shift();
119
+ }
120
+ return cleaned.join('.');
121
+ }
122
+ function matchFirst(sql, pattern) {
123
+ if (!sql)
124
+ return null;
125
+ const match = sql.match(pattern);
126
+ return match?.[1] ?? null;
127
+ }
128
+ function parseTypeArgs(sqlType) {
129
+ const match = sqlType.match(/^(\w+)(?:\(([^)]+)\))?/);
130
+ const typeArgs = match?.[2];
131
+ return {
132
+ base: match?.[1]?.toUpperCase() ?? parseSqlType(sqlType),
133
+ args: typeArgs ? typeArgs.split(',').map(arg => arg.trim().toUpperCase()) : [],
134
+ };
135
+ }
136
+ function numericArg(value) {
137
+ if (!value)
138
+ return undefined;
139
+ if (value === 'MAX')
140
+ return 'MAX';
141
+ const n = Number.parseInt(value, 10);
142
+ return Number.isFinite(n) ? n : undefined;
143
+ }
144
+ function quoteColumnName(raw) {
145
+ return `[${raw.replace(/]/g, ']]')}]`;
146
+ }
147
+ function escapeSqlString(raw) {
148
+ return raw.replace(/'/g, "''");
149
+ }
150
+ function buildAlterColumnWarnings(previousSqlType, nextSqlType, previousNullable, nextNullable) {
151
+ const warnings = [];
152
+ const previous = parseTypeArgs(previousSqlType);
153
+ const next = parseTypeArgs(nextSqlType);
154
+ if (previous.base !== next.base) {
155
+ warnings.push(`Column type family changes from ${previous.base} to ${next.base}; verify existing data can convert.`);
156
+ }
157
+ const previousSize = numericArg(previous.args[0]);
158
+ const nextSize = numericArg(next.args[0]);
159
+ const sizedTypes = new Set(['NVARCHAR', 'NCHAR', 'VARCHAR', 'CHAR', 'VARBINARY', 'BINARY']);
160
+ if (previous.base === next.base && sizedTypes.has(previous.base)) {
161
+ if (previousSize === 'MAX' && typeof nextSize === 'number') {
162
+ warnings.push(`Column size changes from ${previousSqlType} to ${nextSqlType}; existing values may be truncated or block migration.`);
163
+ }
164
+ else if (typeof previousSize === 'number' && typeof nextSize === 'number' && nextSize < previousSize) {
165
+ warnings.push(`Column size shrinks from ${previousSqlType} to ${nextSqlType}; existing values may be truncated or block migration.`);
166
+ }
167
+ }
168
+ const precisionTypes = new Set(['DECIMAL', 'NUMERIC']);
169
+ if (previous.base === next.base && precisionTypes.has(previous.base)) {
170
+ const previousPrecision = numericArg(previous.args[0]);
171
+ const nextPrecision = numericArg(next.args[0]);
172
+ const previousScale = numericArg(previous.args[1]);
173
+ const nextScale = numericArg(next.args[1]);
174
+ if (typeof previousPrecision === 'number' &&
175
+ typeof nextPrecision === 'number' &&
176
+ nextPrecision < previousPrecision) {
177
+ warnings.push(`Column precision shrinks from ${previousSqlType} to ${nextSqlType}; existing numeric values may not fit.`);
178
+ }
179
+ if (typeof previousScale === 'number' && typeof nextScale === 'number' && nextScale < previousScale) {
180
+ warnings.push(`Column scale shrinks from ${previousSqlType} to ${nextSqlType}; existing numeric values may lose fractional precision.`);
181
+ }
182
+ }
183
+ if (previousNullable && !nextNullable) {
184
+ warnings.push('Column changes from NULL to NOT NULL; existing NULL values must be cleaned before applying.');
185
+ }
186
+ return warnings;
187
+ }
188
+ function buildAlterColumnPreflightSql(tableName, columnName, previousSqlType, nextSqlType, previousNullable, nextNullable) {
189
+ const checks = [];
190
+ const table = quoteTableName(tableName);
191
+ const col = quoteColumnName(columnName);
192
+ const label = `${tableName}.${columnName}`;
193
+ const previous = parseTypeArgs(previousSqlType);
194
+ const next = parseTypeArgs(nextSqlType);
195
+ if (previousNullable && !nextNullable) {
196
+ checks.push(`IF EXISTS (SELECT 1 FROM ${table} WHERE ${col} IS NULL)
197
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} contains NULL values.', 1`);
198
+ }
199
+ const previousSize = numericArg(previous.args[0]);
200
+ const nextSize = numericArg(next.args[0]);
201
+ const stringTypes = new Set(['NVARCHAR', 'NCHAR', 'VARCHAR', 'CHAR']);
202
+ const binaryTypes = new Set(['VARBINARY', 'BINARY']);
203
+ if (previous.base === next.base && stringTypes.has(previous.base)) {
204
+ if ((previousSize === 'MAX' && typeof nextSize === 'number') || (typeof previousSize === 'number' && typeof nextSize === 'number' && nextSize < previousSize)) {
205
+ checks.push(`IF EXISTS (SELECT 1 FROM ${table} WHERE ${col} IS NOT NULL AND LEN(${col}) > ${nextSize})
206
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} has values longer than ${nextSize}.', 1`);
207
+ }
208
+ }
209
+ if (previous.base === next.base && binaryTypes.has(previous.base)) {
210
+ if ((previousSize === 'MAX' && typeof nextSize === 'number') || (typeof previousSize === 'number' && typeof nextSize === 'number' && nextSize < previousSize)) {
211
+ checks.push(`IF EXISTS (SELECT 1 FROM ${table} WHERE ${col} IS NOT NULL AND DATALENGTH(${col}) > ${nextSize})
212
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} has binary values longer than ${nextSize} bytes.', 1`);
213
+ }
214
+ }
215
+ const precisionTypes = new Set(['DECIMAL', 'NUMERIC']);
216
+ const previousPrecision = numericArg(previous.args[0]);
217
+ const nextPrecision = numericArg(next.args[0]);
218
+ const previousScale = numericArg(previous.args[1]);
219
+ const nextScale = numericArg(next.args[1]);
220
+ const precisionShrinks = previous.base === next.base &&
221
+ precisionTypes.has(previous.base) &&
222
+ ((typeof previousPrecision === 'number' && typeof nextPrecision === 'number' && nextPrecision < previousPrecision) ||
223
+ (typeof previousScale === 'number' && typeof nextScale === 'number' && nextScale < previousScale));
224
+ if (previous.base !== next.base || precisionShrinks) {
225
+ checks.push(`IF EXISTS (SELECT 1 FROM ${table} WHERE ${col} IS NOT NULL AND TRY_CONVERT(${nextSqlType}, ${col}) IS NULL)
226
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} contains values that cannot convert to ${escapeSqlString(nextSqlType)}.', 1`);
227
+ }
228
+ return checks;
229
+ }
230
+ function buildAddColumnPreflightSql(tableName, field) {
231
+ const checks = [];
232
+ const table = quoteTableName(tableName);
233
+ const label = `${tableName}.${field.name}`;
234
+ if (!field.isOptional && !field.defaultValue && !field.isId) {
235
+ checks.push(`IF EXISTS (SELECT 1 FROM ${table})
236
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} is NOT NULL without a default on a non-empty table.', 1`);
237
+ }
238
+ if (field.isUnique && !field.isId) {
239
+ checks.push(`IF (SELECT COUNT_BIG(*) FROM ${table}) > 1
240
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} is a new UNIQUE column; existing rows would receive duplicate NULL values.', 1`);
241
+ }
242
+ return checks;
243
+ }
244
+ function buildUniqueConstraintPreflightSql(tableName, fields) {
245
+ if (fields.length === 0)
246
+ return [];
247
+ const table = quoteTableName(tableName);
248
+ const columns = fields.map(field => quoteColumnName(field));
249
+ const columnsSql = columns.join(', ');
250
+ const label = `${tableName}.${fields.join(',')}`;
251
+ return [`IF EXISTS (
252
+ SELECT 1
253
+ FROM ${table}
254
+ GROUP BY ${columnsSql}
255
+ HAVING COUNT_BIG(*) > 1
256
+ )
257
+ THROW 51000, 'an5 migration preflight failed: ${escapeSqlString(label)} has duplicate values for a UNIQUE constraint.', 1`];
258
+ }
259
+ /**
260
+ * Reads a `.an5` schema into models to compare against the database.
261
+ *
262
+ * `provider` decides what counts as a column and what counts as a relation; it
263
+ * defaults to SQL Server. A type the provider does not have is reported rather
264
+ * than skipped — skipping it means the migration silently never mentions that
265
+ * column. A token matching a model in the schema is a relation, so that
266
+ * comparison waits until the whole schema has been read.
267
+ */
268
+ function parseSchemaText(text, provider = field_types_1.DEFAULT_PROVIDER) {
269
+ const models = [];
270
+ const typeIssues = [];
271
+ const modelRefs = [];
272
+ /** `@@schema` per model, applied after the model is read rather than mid-loop. */
273
+ const schemas = new Map();
274
+ const lines = text.split('\n');
275
+ let current = null;
276
+ for (let line of lines) {
277
+ line = line.trim();
278
+ if (!line || line.startsWith('//'))
279
+ continue;
280
+ const modelMatch = line.match(/^model\s+(\w+)\s*\{/);
281
+ if (modelMatch) {
282
+ const modelName = modelMatch[1] ?? 'Model';
283
+ current = {
284
+ name: modelName,
285
+ tableName: modelName.toLowerCase() + 's',
286
+ fields: [],
287
+ compoundUniques: [],
288
+ indexes: [],
289
+ };
290
+ models.push(current);
291
+ continue;
292
+ }
293
+ if (line === '}') {
294
+ current = null;
295
+ continue;
296
+ }
297
+ if (!current)
298
+ continue;
299
+ if (line.startsWith('@@map')) {
300
+ // Just the table name. A schema comes from `@@schema` alone, because a mapped
301
+ // name may legitimately contain a dot: `@@map("reports.daily")` is one table
302
+ // whose name has a dot in it, not a table called `daily` in a schema called
303
+ // `reports`.
304
+ const m = line.match(/@@map\("(.+)"\)/);
305
+ if (m?.[1])
306
+ current.tableName = m[1];
307
+ continue;
308
+ }
309
+ if (line.startsWith('@@schema')) {
310
+ // The directive used to be dropped here while `db:push` and the generators
311
+ // honoured it, so a migration created the table in the connection's default
312
+ // schema and the client then read the one the schema file named. Composed in
313
+ // once the whole model has been read, below.
314
+ const m = line.match(/@@schema\("(.*)"\)/);
315
+ if (m !== null)
316
+ schemas.set(current, m[1]?.trim() ?? '');
317
+ continue;
318
+ }
319
+ if (line.startsWith('@@unique')) {
320
+ const parsed = parseMappedIndex(line, '@@unique');
321
+ if (parsed)
322
+ current.compoundUniques.push(parsed);
323
+ continue;
324
+ }
325
+ if (line.startsWith('@@index')) {
326
+ const parsed = parseMappedIndex(line, '@@index');
327
+ if (parsed)
328
+ current.indexes.push(parsed);
329
+ continue;
330
+ }
331
+ if (line.startsWith('@@'))
332
+ continue;
333
+ const head = (0, field_types_1.readFieldLineHead)(line);
334
+ if (!head)
335
+ continue;
336
+ if (!(0, field_types_1.resolveFieldType)(head.type, provider)) {
337
+ modelRefs.push({ model: current.name, field: head.name, type: head.type });
338
+ continue;
339
+ }
340
+ current.fields.push({
341
+ name: head.name,
342
+ sqlType: head.type.toUpperCase(),
343
+ isOptional: head.isOptional,
344
+ isId: line.includes('@id'),
345
+ isUnique: line.includes('@unique'),
346
+ uniqueName: line.match(/@unique\([^)]*\bmap\s*:\s*"([^"]+)"/)?.[1],
347
+ // `@updatedAt` is `now()` under another name, and db:push treats it as such.
348
+ // Without this here a stamp column looked like it had no default and every
349
+ // diff wanted to add one.
350
+ defaultValue: line.match(/@default\((.*)\)/)?.[1]
351
+ ?? (line.includes('@updatedAt') ? 'now()' : undefined),
352
+ });
353
+ }
354
+ // `dbo` stays implicit: an unqualified model produces the SQL it always has, and a
355
+ // named one is qualified with the schema the schema file asked for.
356
+ for (const model of models) {
357
+ const schema = schemas.get(model) ?? '';
358
+ if (schema && schema !== 'dbo')
359
+ model.tableName = `${schema}.${model.tableName}`;
360
+ }
361
+ const modelNames = models.map((model) => model.name);
362
+ for (const ref of modelRefs) {
363
+ if (!modelNames.includes(ref.type)) {
364
+ typeIssues.push({
365
+ path: `${ref.model}.${ref.field}`,
366
+ message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, modelNames),
367
+ });
368
+ }
369
+ }
370
+ if (typeIssues.length > 0)
371
+ throw new field_types_1.FieldTypeError(provider, typeIssues);
372
+ return models;
373
+ }
374
+ function buildCreateTableSql(model) {
375
+ const colDefs = model.fields.map(f => {
376
+ let def = `[${f.name}] ${f.sqlType}`;
377
+ if (f.isId)
378
+ def += ' PRIMARY KEY';
379
+ if (f.defaultValue)
380
+ def += ` ${mapDefault(f.defaultValue)}`;
381
+ if (!f.isOptional && !f.defaultValue && !f.isId)
382
+ def += ' NOT NULL';
383
+ if (f.isUnique && !f.isId)
384
+ def += f.uniqueName ? ` CONSTRAINT [${f.uniqueName}] UNIQUE` : ' UNIQUE';
385
+ return def;
386
+ });
387
+ for (let idx = 0; idx < model.compoundUniques.length; idx++) {
388
+ const definition = model.compoundUniques[idx];
389
+ if (definition === undefined)
390
+ continue;
391
+ const fields = schemaIndexFields(definition);
392
+ const constraintName = schemaIndexName(definition, () => compoundUniqueConstraintName(model, idx));
393
+ const fieldsStr = fields.map(f => `[${f}]`).join(', ');
394
+ colDefs.push(`CONSTRAINT [${constraintName}] UNIQUE (${fieldsStr})`);
395
+ }
396
+ return `CREATE TABLE ${quoteTableName(model.tableName)} (\n ${colDefs.join(',\n ')}\n)`;
397
+ }
398
+ function buildIndexDiff(model, artifacts, ops) {
399
+ const existingIndexes = new Set((artifacts.indexes || []).map(name => name.toLowerCase()));
400
+ const existingUniques = new Set((artifacts.uniqueConstraints || []).map(name => name.toLowerCase()));
401
+ const expectedIndexes = new Set();
402
+ const expectedUniques = new Set();
403
+ for (const field of model.fields) {
404
+ if (!field.isUnique || field.isId)
405
+ continue;
406
+ const constraintName = field.uniqueName || fieldUniqueConstraintName(model, field);
407
+ expectedUniques.add(constraintName.toLowerCase());
408
+ if (!existingUniques.has(constraintName.toLowerCase())) {
409
+ ops.push({
410
+ type: 'ADD_UNIQUE',
411
+ table: model.tableName,
412
+ column: field.name,
413
+ details: field.name,
414
+ sql: `ALTER TABLE ${quoteTableName(model.tableName)} ADD CONSTRAINT [${constraintName}] UNIQUE ([${field.name}])`,
415
+ preflightSql: buildUniqueConstraintPreflightSql(model.tableName, [field.name]),
416
+ });
417
+ }
418
+ }
419
+ for (let idx = 0; idx < model.compoundUniques.length; idx++) {
420
+ const definition = model.compoundUniques[idx];
421
+ if (definition === undefined)
422
+ continue;
423
+ const fields = schemaIndexFields(definition);
424
+ const constraintName = schemaIndexName(definition, () => compoundUniqueConstraintName(model, idx));
425
+ expectedUniques.add(constraintName.toLowerCase());
426
+ if (!existingUniques.has(constraintName.toLowerCase())) {
427
+ const fieldsStr = fields.map(f => `[${f}]`).join(', ');
428
+ ops.push({
429
+ type: 'ADD_UNIQUE',
430
+ table: model.tableName,
431
+ details: fields.join(', '),
432
+ sql: `ALTER TABLE ${quoteTableName(model.tableName)} ADD CONSTRAINT [${constraintName}] UNIQUE (${fieldsStr})`,
433
+ preflightSql: buildUniqueConstraintPreflightSql(model.tableName, fields),
434
+ });
435
+ }
436
+ }
437
+ for (const definition of model.indexes) {
438
+ const fields = schemaIndexFields(definition);
439
+ const name = schemaIndexName(definition, fields => indexName(model, fields));
440
+ expectedIndexes.add(name.toLowerCase());
441
+ if (!existingIndexes.has(name.toLowerCase())) {
442
+ const fieldsStr = fields.map(f => `[${f}]`).join(', ');
443
+ const includeFields = Array.isArray(definition) ? [] : definition.includeFields || [];
444
+ const includeSql = includeFields.length > 0 ? ` INCLUDE (${includeFields.map(f => `[${f}]`).join(', ')})` : '';
445
+ const filter = Array.isArray(definition) ? undefined : definition.filter;
446
+ const filterSql = filter ? ` WHERE ${filter}` : '';
447
+ const options = Array.isArray(definition) ? undefined : definition.options;
448
+ const optionsSql = options ? ` WITH (${options})` : '';
449
+ ops.push({
450
+ type: 'ADD_INDEX',
451
+ table: model.tableName,
452
+ details: [
453
+ fields.join(', '),
454
+ includeFields.length > 0 ? `include ${includeFields.join(', ')}` : '',
455
+ filter ? `filter ${filter}` : '',
456
+ options ? `options ${options}` : '',
457
+ ].filter(Boolean).join(' '),
458
+ sql: `CREATE INDEX [${name}] ON ${quoteTableName(model.tableName)} (${fieldsStr})${includeSql}${filterSql}${optionsSql}`,
459
+ });
460
+ }
461
+ }
462
+ const managedIndexPrefix = `ix_${safeIdentifierName(model.tableName).toLowerCase()}_`;
463
+ for (const name of artifacts.indexes || []) {
464
+ const normalized = name.toLowerCase();
465
+ if (normalized.startsWith(managedIndexPrefix) && !expectedIndexes.has(normalized)) {
466
+ ops.push({
467
+ type: 'DROP_INDEX',
468
+ table: model.tableName,
469
+ details: 'Index not in schema',
470
+ sql: `-- DROP INDEX [${name.replace(/]/g, ']]')}] ON ${quoteTableName(model.tableName)}`,
471
+ });
472
+ }
473
+ }
474
+ const managedUniquePrefix = `uq_${safeIdentifierName(model.tableName).toLowerCase()}_`;
475
+ for (const name of artifacts.uniqueConstraints || []) {
476
+ const normalized = name.toLowerCase();
477
+ if (normalized.startsWith(managedUniquePrefix) && !expectedUniques.has(normalized)) {
478
+ ops.push({
479
+ type: 'DROP_UNIQUE',
480
+ table: model.tableName,
481
+ details: 'Unique constraint not in schema',
482
+ sql: `-- ALTER TABLE ${quoteTableName(model.tableName)} DROP CONSTRAINT [${name.replace(/]/g, ']]')}]`,
483
+ });
484
+ }
485
+ }
486
+ }
487
+ function generateColumnDiff(model, dbColumns, ops) {
488
+ const dbColumnMap = new Map(dbColumns.map(c => [c.columnName.toLowerCase(), c]));
489
+ const schemaFields = model.fields;
490
+ for (const f of schemaFields) {
491
+ const existing = dbColumnMap.get(f.name.toLowerCase());
492
+ if (!existing) {
493
+ let def = `[${f.name}] ${f.sqlType}`;
494
+ if (f.isId)
495
+ def += ' PRIMARY KEY';
496
+ if (f.defaultValue)
497
+ def += ` ${mapDefault(f.defaultValue)}`;
498
+ if (!f.isOptional && !f.defaultValue && !f.isId)
499
+ def += ' NOT NULL';
500
+ if (f.isUnique && !f.isId)
501
+ def += f.uniqueName ? ` CONSTRAINT [${f.uniqueName}] UNIQUE` : ' UNIQUE';
502
+ ops.push({
503
+ type: 'ADD_COLUMN',
504
+ table: model.tableName,
505
+ column: f.name,
506
+ sql: `ALTER TABLE ${quoteTableName(model.tableName)} ADD ${def}`,
507
+ preflightSql: buildAddColumnPreflightSql(model.tableName, f),
508
+ });
509
+ continue;
510
+ }
511
+ const schemaType = f.sqlType.toUpperCase();
512
+ const dbType = formatDbColumnSqlType(existing);
513
+ const schemaNullable = f.isOptional;
514
+ const dbNullable = Boolean(existing.isNullable);
515
+ const typeChanged = schemaType !== dbType;
516
+ const nullableChanged = schemaNullable !== dbNullable;
517
+ if (typeChanged || nullableChanged) {
518
+ const def = `[${f.name}] ${f.sqlType}`;
519
+ const riskWarnings = buildAlterColumnWarnings(dbType, schemaType, dbNullable, schemaNullable);
520
+ const preflightSql = buildAlterColumnPreflightSql(model.tableName, f.name, dbType, schemaType, dbNullable, schemaNullable);
521
+ ops.push({
522
+ type: 'ALTER_COLUMN',
523
+ table: model.tableName,
524
+ column: f.name,
525
+ details: typeChanged
526
+ ? `type ${dbType} -> ${schemaType}${nullableChanged ? `, nullable ${dbNullable} -> ${schemaNullable}` : ''}`
527
+ : `nullable ${dbNullable} -> ${schemaNullable}`,
528
+ sql: `ALTER TABLE ${quoteTableName(model.tableName)} ALTER COLUMN ${def}`,
529
+ previousSqlType: dbType,
530
+ previousNullable: dbNullable,
531
+ riskWarnings,
532
+ preflightSql,
533
+ });
534
+ }
535
+ }
536
+ const schemaColumnNames = new Set(schemaFields.map(f => f.name.toLowerCase()));
537
+ for (const c of dbColumns) {
538
+ if (!schemaColumnNames.has(c.columnName.toLowerCase())) {
539
+ ops.push({
540
+ type: 'DROP_COLUMN',
541
+ table: model.tableName,
542
+ column: c.columnName,
543
+ details: 'Column not in schema',
544
+ sql: `-- ALTER TABLE ${quoteTableName(model.tableName)} DROP COLUMN [${c.columnName}]`,
545
+ });
546
+ }
547
+ }
548
+ }
549
+ async function generateDiff(schemaModels, dbTables, introspectTable, introspectArtifacts) {
550
+ const ops = [];
551
+ const dbTableIdentities = new Set(dbTables.map(tableIdentityName));
552
+ const schemaTableNames = new Set(schemaModels.map(m => tableIdentityName(m.tableName)));
553
+ for (const model of schemaModels) {
554
+ if (!dbTableIdentities.has(tableIdentityName(model.tableName))) {
555
+ ops.push({ type: 'CREATE_TABLE', table: model.tableName, sql: buildCreateTableSql(model) });
556
+ buildIndexDiff(model, {
557
+ indexes: [],
558
+ uniqueConstraints: [
559
+ ...model.fields.filter(field => field.isUnique && !field.isId).map(field => field.uniqueName || fieldUniqueConstraintName(model, field)),
560
+ ...model.compoundUniques.map((definition, idx) => schemaIndexName(definition, () => compoundUniqueConstraintName(model, idx))),
561
+ ],
562
+ }, ops);
563
+ }
564
+ }
565
+ for (const model of schemaModels) {
566
+ if (dbTableIdentities.has(tableIdentityName(model.tableName))) {
567
+ const dbColumns = await introspectTable(model.tableName);
568
+ generateColumnDiff(model, dbColumns, ops);
569
+ if (introspectArtifacts) {
570
+ buildIndexDiff(model, await introspectArtifacts(model.tableName), ops);
571
+ }
572
+ }
573
+ }
574
+ for (const tableName of dbTables) {
575
+ if (!schemaTableNames.has(tableIdentityName(tableName))) {
576
+ ops.push({
577
+ type: 'DROP_TABLE',
578
+ table: tableName,
579
+ details: 'Table not in schema',
580
+ sql: `-- DROP TABLE ${quoteTableName(tableName)}`,
581
+ });
582
+ }
583
+ }
584
+ return ops;
585
+ }
586
+ function buildMigrationFile(timestamp, ops) {
587
+ const preflightSql = ops.flatMap(op => op.preflightSql || []);
588
+ const lines = [
589
+ `-- Migration: ${timestamp}`,
590
+ '-- Generated by an5Orm migrate',
591
+ '',
592
+ ];
593
+ if (preflightSql.length > 0) {
594
+ lines.push('-- migrate:preflight');
595
+ lines.push('');
596
+ for (const sql of preflightSql) {
597
+ lines.push(sql);
598
+ lines.push('GO');
599
+ }
600
+ lines.push('');
601
+ }
602
+ lines.push('-- migrate:up');
603
+ lines.push('');
604
+ for (const op of ops) {
605
+ lines.push(`-- ${op.type}: ${op.table}`);
606
+ for (const warning of op.riskWarnings || []) {
607
+ lines.push(`-- WARNING: ${warning}`);
608
+ }
609
+ if (op.sql)
610
+ lines.push(op.sql);
611
+ lines.push('');
612
+ }
613
+ lines.push('-- migrate:down');
614
+ const downSql = buildDownMigrationSql(ops);
615
+ if (downSql.trim()) {
616
+ lines.push(downSql);
617
+ }
618
+ else {
619
+ lines.push('-- Add rollback SQL here. Leave empty to make rollback explicit and non-destructive.');
620
+ }
621
+ lines.push('');
622
+ return lines.join('\n');
623
+ }
624
+ function buildDownMigrationSql(ops) {
625
+ const lines = [];
626
+ for (const op of [...ops].reverse()) {
627
+ const table = quoteTableName(op.table);
628
+ if (op.type === 'CREATE_TABLE') {
629
+ lines.push(`DROP TABLE ${table}`);
630
+ continue;
631
+ }
632
+ if (op.type === 'ADD_COLUMN' && op.column) {
633
+ lines.push(`ALTER TABLE ${table} DROP COLUMN [${op.column}]`);
634
+ continue;
635
+ }
636
+ if (op.type === 'ADD_INDEX') {
637
+ const indexName = matchFirst(op.sql, /CREATE\s+INDEX\s+\[([^\]]+)]/i);
638
+ if (indexName)
639
+ lines.push(`DROP INDEX [${indexName}] ON ${table}`);
640
+ continue;
641
+ }
642
+ if (op.type === 'ADD_UNIQUE') {
643
+ const constraintName = matchFirst(op.sql, /ADD\s+CONSTRAINT\s+\[([^\]]+)]/i);
644
+ if (constraintName)
645
+ lines.push(`ALTER TABLE ${table} DROP CONSTRAINT [${constraintName}]`);
646
+ continue;
647
+ }
648
+ if (op.sql && !op.sql.trim().startsWith('--')) {
649
+ if (op.type === 'ALTER_COLUMN' && op.column && op.previousSqlType && typeof op.previousNullable === 'boolean') {
650
+ lines.push(`ALTER TABLE ${table} ALTER COLUMN [${op.column}] ${op.previousSqlType}${op.previousNullable ? ' NULL' : ' NOT NULL'}`);
651
+ continue;
652
+ }
653
+ lines.push(`-- Manual rollback required for ${op.type}: ${op.table}${op.column ? `.${op.column}` : ''}`);
654
+ }
655
+ }
656
+ return lines.join('\nGO\n');
657
+ }
658
+ function parseMigrationSections(sql) {
659
+ const markerPattern = /^\s*--\s*migrate:(preflight|up|down)\s*$/gim;
660
+ const markers = [];
661
+ let match;
662
+ while ((match = markerPattern.exec(sql))) {
663
+ markers.push({ name: (match[1] ?? 'up').toLowerCase(), index: match.index, end: match.index + match[0].length });
664
+ }
665
+ if (markers.length === 0) {
666
+ return { preflight: '', up: sql.trim(), down: '', hasDown: false };
667
+ }
668
+ const section = (name) => {
669
+ const markerIndex = markers.findIndex(marker => marker.name === name);
670
+ if (markerIndex < 0)
671
+ return '';
672
+ const marker = markers[markerIndex];
673
+ if (!marker)
674
+ return '';
675
+ const start = marker.end;
676
+ const end = markers[markerIndex + 1]?.index ?? sql.length;
677
+ return sql.slice(start, end).trim();
678
+ };
679
+ return {
680
+ preflight: section('preflight'),
681
+ up: section('up'),
682
+ down: section('down'),
683
+ hasDown: markers.some(marker => marker.name === 'down'),
684
+ };
685
+ }
686
+ function splitSqlBatches(sql) {
687
+ return sql
688
+ .split(/^\s*GO\s*;?\s*$/gim)
689
+ .map(batch => batch.trim())
690
+ .filter(batch => {
691
+ if (!batch)
692
+ return false;
693
+ const nonCommentLines = batch
694
+ .split(/\r?\n/)
695
+ .map(line => line.trim())
696
+ .filter(line => line && !line.startsWith('--'));
697
+ return nonCommentLines.length > 0;
698
+ });
699
+ }
700
+ function parseRollbackSelection(args, applied) {
701
+ if (args.length === 0)
702
+ return { count: 1, label: 'latest migration' };
703
+ if (args[0] === '--to') {
704
+ const target = args[1];
705
+ if (!target)
706
+ throw new Error('Rollback target is required after --to.');
707
+ const index = applied.findIndex(row => row.id === target);
708
+ if (index < 0)
709
+ throw new Error(`Rollback target is not applied: ${target}`);
710
+ return { count: applied.length - index, label: `through ${target}` };
711
+ }
712
+ const count = Number.parseInt(args[0] ?? '', 10);
713
+ if (!Number.isFinite(count) || count < 1) {
714
+ throw new Error('Rollback steps must be a positive integer, or use --to <migration-file>.');
715
+ }
716
+ return { count, label: `${count} migration${count === 1 ? '' : 's'}` };
717
+ }
718
+ function parseMigrationCommandOptions(args) {
719
+ const rest = [];
720
+ let dryRun = false;
721
+ for (const arg of args) {
722
+ if (arg === '--dry-run') {
723
+ dryRun = true;
724
+ }
725
+ else {
726
+ rest.push(arg);
727
+ }
728
+ }
729
+ return { dryRun, rest };
730
+ }
731
+ //# sourceMappingURL=migration-core.js.map