@an5/orm 1.0.8 → 1.0.9

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