@an5/orm 1.3.1 → 1.3.3

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 (62) hide show
  1. package/README.md +74 -11
  2. package/dist/cleanup.d.ts +1 -0
  3. package/dist/cleanup.js +177 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +147 -0
  6. package/dist/generator/src/attributes.d.ts +57 -0
  7. package/dist/generator/src/attributes.js +225 -0
  8. package/dist/generator/src/code-generator.js +49 -16
  9. package/dist/generator/src/config.d.ts +2 -2
  10. package/dist/generator/src/config.js +4 -3
  11. package/dist/generator/src/dialect.d.ts +52 -1
  12. package/dist/generator/src/dialect.js +136 -3
  13. package/dist/generator/src/dotnet-generator.js +107 -27
  14. package/dist/generator/src/golang-generator.d.ts +8 -0
  15. package/dist/generator/src/golang-generator.js +116 -49
  16. package/dist/generator/src/java-generator.d.ts +8 -0
  17. package/dist/generator/src/java-generator.js +108 -3
  18. package/dist/generator/src/kotlin-generator.js +51 -0
  19. package/dist/generator/src/parser.d.ts +1 -0
  20. package/dist/generator/src/parser.js +90 -35
  21. package/dist/generator/src/pull-schema.d.ts +54 -0
  22. package/dist/generator/src/pull-schema.js +116 -0
  23. package/dist/generator/src/push-apply.d.ts +4 -2
  24. package/dist/generator/src/push-apply.js +53 -4
  25. package/dist/generator/src/push-schema.d.ts +21 -9
  26. package/dist/generator/src/push-schema.js +132 -32
  27. package/dist/generator/src/python-generator.js +35 -22
  28. package/dist/generator/src/rust-generator.js +79 -74
  29. package/dist/generator/src/swift-generator.js +54 -3
  30. package/dist/generator/src/types.d.ts +10 -0
  31. package/dist/generator/src/view-order.d.ts +33 -0
  32. package/dist/generator/src/view-order.js +123 -0
  33. package/dist/migrate.d.ts +12 -0
  34. package/dist/migrate.js +442 -0
  35. package/dist/migration-core.d.ts +32 -3
  36. package/dist/migration-core.js +213 -43
  37. package/dist/provider-support.d.ts +0 -1
  38. package/dist/provider-support.js +0 -1
  39. package/dist/pull.d.ts +1 -0
  40. package/dist/pull.js +425 -0
  41. package/dist/push.d.ts +1 -0
  42. package/dist/push.js +93 -0
  43. package/dist/seed.d.ts +1 -0
  44. package/dist/seed.js +33 -0
  45. package/generator/src/attributes.ts +231 -0
  46. package/generator/src/code-generator.ts +47 -16
  47. package/generator/src/config.ts +5 -3
  48. package/generator/src/dialect.ts +188 -5
  49. package/generator/src/dotnet-generator.ts +108 -28
  50. package/generator/src/golang-generator.ts +119 -50
  51. package/generator/src/java-generator.ts +109 -3
  52. package/generator/src/kotlin-generator.ts +51 -0
  53. package/generator/src/parser.ts +102 -30
  54. package/generator/src/pull-schema.ts +127 -0
  55. package/generator/src/push-apply.ts +62 -5
  56. package/generator/src/push-schema.ts +160 -32
  57. package/generator/src/python-generator.ts +33 -20
  58. package/generator/src/rust-generator.ts +79 -73
  59. package/generator/src/swift-generator.ts +55 -4
  60. package/generator/src/types.ts +10 -0
  61. package/generator/src/view-order.ts +133 -0
  62. package/package.json +19 -14
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * an5Orm Migration Tool
8
+ * Compares schema files with database and generates migration SQL.
9
+ *
10
+ * Usage:
11
+ * npx tsx migrate.ts diff # Show differences
12
+ * npx tsx migrate.ts generate # Generate migration file
13
+ * npx tsx migrate.ts apply # Apply pending migrations
14
+ * npx tsx migrate.ts rollback [steps|--to file] # Roll back applied migrations
15
+ * npx tsx migrate.ts status # Show migration status
16
+ */
17
+ require("dotenv/config");
18
+ const crypto_1 = __importDefault(require("crypto"));
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const adapters_1 = require("@an5/adapters");
22
+ const config_1 = require("./generator/src/config");
23
+ const field_types_1 = require("./generator/src/field-types");
24
+ const provider_support_1 = require("./provider-support");
25
+ const migration_core_1 = require("./migration-core");
26
+ const rootDir = process.cwd();
27
+ const { config, outputs } = (0, config_1.loadConfig)();
28
+ const schemaDir = outputs.schemaDir;
29
+ const migrationsDir = path_1.default.resolve(rootDir, 'migrations');
30
+ const migrationTableName = '[dbo].[_an5_migrations]';
31
+ let _adapter = null;
32
+ async function getDb() {
33
+ if (!_adapter) {
34
+ _adapter = new adapters_1.An5Adapter({
35
+ connectionString: (0, config_1.resolveConnectionString)(config, process.env, 'db:migrate'),
36
+ });
37
+ await _adapter.$connect();
38
+ }
39
+ return _adapter;
40
+ }
41
+ // ─── Schema Parser ───────────────────────────────────────────────────────────
42
+ function parseSchema() {
43
+ if (!fs_1.default.existsSync(schemaDir))
44
+ return [];
45
+ const files = fs_1.default.readdirSync(schemaDir).filter(f => f.endsWith('.an5'));
46
+ let text = '';
47
+ for (const file of files) {
48
+ text += fs_1.default.readFileSync(path_1.default.join(schemaDir, file), 'utf8') + '\n';
49
+ }
50
+ return (0, migration_core_1.parseSchemaText)(text, (0, config_1.providerFromConfig)(config, process.env));
51
+ }
52
+ // ─── Database Introspection ──────────────────────────────────────────────────
53
+ async function introspectTable(tableName) {
54
+ return (await getDb()).$queryRawUnsafe(`
55
+ SELECT
56
+ c.name AS columnName,
57
+ ty.name AS dataType,
58
+ c.max_length AS maxLength,
59
+ c.precision AS precision,
60
+ c.scale AS scale,
61
+ c.is_nullable AS isNullable,
62
+ pk.is_primary_key AS isPrimaryKey,
63
+ c.is_identity AS isIdentity,
64
+ d.definition AS defaultValue
65
+ FROM sys.columns c
66
+ JOIN sys.types ty ON c.user_type_id = ty.user_type_id
67
+ LEFT JOIN (
68
+ SELECT ic.object_id, ic.column_id, i.is_primary_key
69
+ FROM sys.index_columns ic
70
+ JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
71
+ WHERE i.is_primary_key = 1
72
+ ) pk ON c.object_id = pk.object_id AND c.column_id = pk.column_id
73
+ LEFT JOIN sys.default_constraints d ON c.default_object_id = d.object_id
74
+ WHERE c.object_id = OBJECT_ID(@p_0)
75
+ ORDER BY c.column_id
76
+ `, tableName);
77
+ }
78
+ async function getExistingTables() {
79
+ const rows = await (await getDb()).$queryRawUnsafe(`
80
+ SELECT
81
+ CASE
82
+ WHEN SCHEMA_NAME(schema_id) = 'dbo' THEN name
83
+ ELSE SCHEMA_NAME(schema_id) + '.' + name
84
+ END AS name
85
+ FROM sys.tables
86
+ WHERE is_ms_shipped = 0
87
+ ORDER BY SCHEMA_NAME(schema_id), name
88
+ `);
89
+ return rows.map(r => r.name);
90
+ }
91
+ async function getExistingViews() {
92
+ const rows = await (await getDb()).$queryRawUnsafe(`
93
+ SELECT
94
+ CASE
95
+ WHEN SCHEMA_NAME(schema_id) = 'dbo' THEN name
96
+ ELSE SCHEMA_NAME(schema_id) + '.' + name
97
+ END AS name
98
+ FROM sys.views
99
+ WHERE is_ms_shipped = 0
100
+ ORDER BY SCHEMA_NAME(schema_id), name
101
+ `);
102
+ return rows.map(r => r.name);
103
+ }
104
+ async function introspectViewDefinition(viewName) {
105
+ const rows = await (await getDb()).$queryRawUnsafe(`SELECT OBJECT_DEFINITION(OBJECT_ID(@p_0)) AS definition`, viewName);
106
+ return rows[0]?.definition ?? null;
107
+ }
108
+ function listMigrationFiles() {
109
+ if (!fs_1.default.existsSync(migrationsDir)) {
110
+ fs_1.default.mkdirSync(migrationsDir, { recursive: true });
111
+ }
112
+ return fs_1.default.readdirSync(migrationsDir).filter(f => f.endsWith('.sql')).sort();
113
+ }
114
+ function checksum(content) {
115
+ return crypto_1.default.createHash('sha256').update(content).digest('hex');
116
+ }
117
+ async function ensureMigrationTable() {
118
+ await (await getDb()).$executeRawUnsafe(`
119
+ IF OBJECT_ID('dbo._an5_migrations', 'U') IS NULL
120
+ CREATE TABLE ${migrationTableName} (
121
+ [id] NVARCHAR(255) NOT NULL PRIMARY KEY,
122
+ [checksum] NVARCHAR(64) NOT NULL,
123
+ [appliedAt] DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
124
+ )
125
+ `);
126
+ }
127
+ async function migrationTableExists() {
128
+ const rows = await (await getDb()).$queryRawUnsafe(`SELECT name FROM sys.tables WHERE object_id = OBJECT_ID('dbo._an5_migrations')`);
129
+ return rows.length > 0;
130
+ }
131
+ async function getAppliedMigrations(options = {}) {
132
+ if (options.ensure) {
133
+ await ensureMigrationTable();
134
+ }
135
+ else if (!(await migrationTableExists())) {
136
+ return [];
137
+ }
138
+ return (await getDb()).$queryRawUnsafe(`SELECT [id], [checksum], [appliedAt] FROM ${migrationTableName} ORDER BY [id] ASC`);
139
+ }
140
+ async function executeBatches(executor, sql) {
141
+ for (const batch of (0, migration_core_1.splitSqlBatches)(sql)) {
142
+ if (executor._executeRaw) {
143
+ await executor._executeRaw(batch);
144
+ }
145
+ else if (executor.$executeRawUnsafe) {
146
+ await executor.$executeRawUnsafe(batch);
147
+ }
148
+ else {
149
+ throw new Error('Migration executor does not support raw execution.');
150
+ }
151
+ }
152
+ }
153
+ function printBatches(title, sql) {
154
+ const batches = (0, migration_core_1.splitSqlBatches)(sql);
155
+ console.log(title);
156
+ for (let i = 0; i < batches.length; i++) {
157
+ console.log(`-- batch ${i + 1}`);
158
+ console.log(batches[i]);
159
+ }
160
+ }
161
+ async function introspectTableArtifacts(tableName) {
162
+ const indexes = await (await getDb()).$queryRawUnsafe(`
163
+ SELECT name
164
+ FROM sys.indexes
165
+ WHERE object_id = OBJECT_ID(@p_0)
166
+ AND is_primary_key = 0
167
+ AND is_unique_constraint = 0
168
+ AND name IS NOT NULL
169
+ `, tableName);
170
+ const uniqueConstraints = await (await getDb()).$queryRawUnsafe(`
171
+ SELECT name
172
+ FROM sys.objects
173
+ WHERE type = 'UQ'
174
+ AND parent_object_id = OBJECT_ID(@p_0)
175
+ `, tableName);
176
+ return {
177
+ indexes: indexes.map(row => row.name),
178
+ uniqueConstraints: uniqueConstraints.map(row => row.name),
179
+ };
180
+ }
181
+ // ─── Diff Engine ─────────────────────────────────────────────────────────────
182
+ // ─── Commands ────────────────────────────────────────────────────────────────
183
+ async function cmdDiff() {
184
+ console.log('\n🔍 Comparing schema with database...\n');
185
+ const schemaModels = parseSchema();
186
+ const dbTables = await getExistingTables();
187
+ const dbViews = await getExistingViews();
188
+ const modelCount = schemaModels.filter(model => !model.isView).length;
189
+ const viewCount = schemaModels.length - modelCount;
190
+ console.log(`Schema: ${modelCount} models, ${viewCount} views`);
191
+ console.log(`Database: ${dbTables.length} tables, ${dbViews.length} views\n`);
192
+ const ops = await (0, migration_core_1.generateDiff)(schemaModels, dbTables, introspectTable, introspectTableArtifacts, dbViews, introspectViewDefinition);
193
+ if (ops.length === 0) {
194
+ console.log('✅ Schema is in sync with database.');
195
+ return;
196
+ }
197
+ console.log(`Found ${ops.length} difference(s):\n`);
198
+ for (const op of ops) {
199
+ console.log(` ${op.type}: ${op.table}`);
200
+ if (op.details)
201
+ console.log(` ${op.details}`);
202
+ if (op.sql)
203
+ console.log(` SQL: ${op.sql}`);
204
+ }
205
+ }
206
+ async function cmdGenerate() {
207
+ console.log('\n📝 Generating migration file...\n');
208
+ const schemaModels = parseSchema();
209
+ const dbTables = await getExistingTables();
210
+ const dbViews = await getExistingViews();
211
+ const ops = await (0, migration_core_1.generateDiff)(schemaModels, dbTables, introspectTable, introspectTableArtifacts, dbViews, introspectViewDefinition);
212
+ if (ops.length === 0) {
213
+ console.log('No migrations needed.');
214
+ return;
215
+ }
216
+ if (!fs_1.default.existsSync(migrationsDir)) {
217
+ fs_1.default.mkdirSync(migrationsDir, { recursive: true });
218
+ }
219
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
220
+ const filename = `${timestamp}_migration.sql`;
221
+ const filepath = path_1.default.join(migrationsDir, filename);
222
+ fs_1.default.writeFileSync(filepath, (0, migration_core_1.buildMigrationFile)(timestamp, ops));
223
+ console.log(`✅ Migration written to: ${filepath}`);
224
+ }
225
+ async function cmdApply(args = []) {
226
+ const options = (0, migration_core_1.parseMigrationCommandOptions)(args);
227
+ console.log(options.preview ? '\n👀 Previewing pending migrations...\n' : '\n🚀 Applying migrations...\n');
228
+ const db = await getDb();
229
+ const appliedRows = await getAppliedMigrations({ ensure: true });
230
+ const applied = new Map(appliedRows.map(row => [row.id, row.checksum]));
231
+ const files = listMigrationFiles();
232
+ // An applied file that changed on disk no longer describes what the database
233
+ // contains. Applying the rest would build on a history that cannot be reproduced.
234
+ const drift = (0, migration_core_1.verifyAppliedMigrations)(appliedRows, (id) => {
235
+ const filepath = path_1.default.join(migrationsDir, id);
236
+ return fs_1.default.existsSync(filepath) ? fs_1.default.readFileSync(filepath, 'utf8') : null;
237
+ }, checksum);
238
+ if (drift.changed.length > 0 || drift.missing.length > 0) {
239
+ const details = [
240
+ ...drift.changed.map(id => ` changed: ${id}`),
241
+ ...drift.missing.map(id => ` missing: ${id}`),
242
+ ].join('\n');
243
+ throw new Error(`Applied migration history does not match the files on disk:\n${details}\n` +
244
+ 'Restore the files, or mark the affected migrations as unapplied deliberately before continuing.');
245
+ }
246
+ const pending = files.filter(file => !applied.has(file));
247
+ if (pending.length === 0) {
248
+ console.log('✅ No pending migrations.');
249
+ return;
250
+ }
251
+ for (const file of pending) {
252
+ const filepath = path_1.default.join(migrationsDir, file);
253
+ const content = fs_1.default.readFileSync(filepath, 'utf8');
254
+ const sections = (0, migration_core_1.parseMigrationSections)(content);
255
+ const digest = checksum(content);
256
+ if ((0, migration_core_1.splitSqlBatches)(sections.up).length === 0) {
257
+ throw new Error(`Migration ${file} has no up SQL to apply.`);
258
+ }
259
+ if (options.preview) {
260
+ if ((0, migration_core_1.splitSqlBatches)(sections.preflight).length > 0) {
261
+ printBatches(`-- ${file} preflight`, sections.preflight);
262
+ }
263
+ printBatches(`-- ${file} up`, sections.up);
264
+ continue;
265
+ }
266
+ await db.$transaction(async (tx) => {
267
+ if ((0, migration_core_1.splitSqlBatches)(sections.preflight).length > 0) {
268
+ await executeBatches(tx, sections.preflight);
269
+ }
270
+ await executeBatches(tx, sections.up);
271
+ await tx._executeRaw(`INSERT INTO ${migrationTableName} ([id], [checksum]) VALUES (@id, @checksum)`, { id: file, checksum: digest });
272
+ });
273
+ console.log(`✅ Applied ${file}`);
274
+ }
275
+ }
276
+ async function rollbackOne(db, migration) {
277
+ const filepath = path_1.default.join(migrationsDir, migration.id);
278
+ if (!fs_1.default.existsSync(filepath)) {
279
+ throw new Error(`Applied migration file is missing: ${migration.id}`);
280
+ }
281
+ const content = fs_1.default.readFileSync(filepath, 'utf8');
282
+ if (checksum(content) !== migration.checksum) {
283
+ throw new Error(`Applied migration checksum changed: ${migration.id}`);
284
+ }
285
+ const sections = (0, migration_core_1.parseMigrationSections)(content);
286
+ if (!sections.hasDown || (0, migration_core_1.splitSqlBatches)(sections.down).length === 0) {
287
+ throw new Error(`Migration ${migration.id} does not contain rollback SQL after -- migrate:down.`);
288
+ }
289
+ await db.$transaction(async (tx) => {
290
+ await executeBatches(tx, sections.down);
291
+ await tx._executeRaw(`DELETE FROM ${migrationTableName} WHERE [id] = @id`, { id: migration.id });
292
+ });
293
+ console.log(`✅ Rolled back ${migration.id}`);
294
+ }
295
+ function readRollbackSql(migration) {
296
+ const filepath = path_1.default.join(migrationsDir, migration.id);
297
+ if (!fs_1.default.existsSync(filepath)) {
298
+ throw new Error(`Applied migration file is missing: ${migration.id}`);
299
+ }
300
+ const content = fs_1.default.readFileSync(filepath, 'utf8');
301
+ if (checksum(content) !== migration.checksum) {
302
+ throw new Error(`Applied migration checksum changed: ${migration.id}`);
303
+ }
304
+ const sections = (0, migration_core_1.parseMigrationSections)(content);
305
+ if (!sections.hasDown || (0, migration_core_1.splitSqlBatches)(sections.down).length === 0) {
306
+ throw new Error(`Migration ${migration.id} does not contain rollback SQL after -- migrate:down.`);
307
+ }
308
+ return sections.down;
309
+ }
310
+ async function cmdRollback(args = []) {
311
+ const options = (0, migration_core_1.parseMigrationCommandOptions)(args);
312
+ const db = await getDb();
313
+ const applied = await getAppliedMigrations({ ensure: true });
314
+ if (applied.length === 0) {
315
+ console.log('✅ No applied migrations to roll back.');
316
+ return;
317
+ }
318
+ const selection = (0, migration_core_1.parseRollbackSelection)(options.rest, applied);
319
+ const targets = applied.slice(Math.max(0, applied.length - selection.count)).reverse();
320
+ console.log(options.preview ? `\n👀 Previewing rollback ${selection.label}...\n` : `\n↩️ Rolling back ${selection.label}...\n`);
321
+ if (targets.length < selection.count) {
322
+ console.log(`Only ${targets.length} applied migration${targets.length === 1 ? '' : 's'} available.`);
323
+ }
324
+ for (const migration of targets) {
325
+ if (options.preview) {
326
+ printBatches(`-- ${migration.id} down`, readRollbackSql(migration));
327
+ }
328
+ else {
329
+ await rollbackOne(db, migration);
330
+ }
331
+ }
332
+ }
333
+ async function cmdStatus() {
334
+ console.log('\n📊 Migration Status\n');
335
+ const schemaModels = parseSchema();
336
+ const dbTables = await getExistingTables();
337
+ const dbViews = await getExistingViews();
338
+ const tableModels = schemaModels.filter(model => !model.isView);
339
+ const viewModels = schemaModels.filter(model => model.isView);
340
+ console.log('Schema Models:');
341
+ const dbTableIdentities = new Set(dbTables.map(migration_core_1.tableIdentityName));
342
+ for (const model of tableModels) {
343
+ const exists = dbTableIdentities.has((0, migration_core_1.tableIdentityName)(model.tableName));
344
+ const icon = exists ? '✅' : '⚠️';
345
+ console.log(` ${icon} ${model.name} → ${model.tableName} (${model.fields.length} fields)`);
346
+ }
347
+ const dbViewIdentities = new Set(dbViews.map(migration_core_1.tableIdentityName));
348
+ if (viewModels.length > 0 || dbViews.length > 0) {
349
+ console.log('\nSchema Views:');
350
+ for (const view of viewModels) {
351
+ const exists = dbViewIdentities.has((0, migration_core_1.tableIdentityName)(view.tableName));
352
+ const icon = exists ? '✅' : '⚠️';
353
+ console.log(` ${icon} ${view.name} → ${view.tableName} (${view.fields.length} fields)`);
354
+ }
355
+ }
356
+ console.log('\nDatabase Tables:');
357
+ const schemaTableIdentities = new Set(tableModels.map(model => (0, migration_core_1.tableIdentityName)(model.tableName)));
358
+ for (const table of dbTables) {
359
+ const inSchema = schemaTableIdentities.has((0, migration_core_1.tableIdentityName)(table));
360
+ const icon = inSchema ? '✅' : '⚠️';
361
+ console.log(` ${icon} ${table}`);
362
+ }
363
+ if (dbViews.length > 0) {
364
+ console.log('\nDatabase Views:');
365
+ const schemaViewIdentities = new Set(viewModels.map(view => (0, migration_core_1.tableIdentityName)(view.tableName)));
366
+ for (const view of dbViews) {
367
+ const inSchema = schemaViewIdentities.has((0, migration_core_1.tableIdentityName)(view));
368
+ const icon = inSchema ? '✅' : '⚠️';
369
+ console.log(` ${icon} ${view}`);
370
+ }
371
+ }
372
+ if (!fs_1.default.existsSync(migrationsDir)) {
373
+ fs_1.default.mkdirSync(migrationsDir, { recursive: true });
374
+ }
375
+ const migrations = listMigrationFiles();
376
+ const applied = await getAppliedMigrations();
377
+ const appliedIds = new Set(applied.map(row => row.id));
378
+ console.log(`\nMigration files: ${migrations.length}`);
379
+ console.log(`Applied migrations: ${applied.length}`);
380
+ console.log(`Pending migrations: ${migrations.filter(file => !appliedIds.has(file)).length}`);
381
+ }
382
+ // ─── Main ────────────────────────────────────────────────────────────────────
383
+ /**
384
+ * Prints why a command cannot run against this provider and exits.
385
+ *
386
+ * The decision and the wording live in `./provider-support`, which stays free of Node
387
+ * globals so it can be imported and tested anywhere; only the command files, which
388
+ * are Node programs, do the exiting.
389
+ */
390
+ function refuseIfNotSqlServer(provider, command) {
391
+ const message = (0, provider_support_1.sqlServerOnlyError)(provider, command);
392
+ if (message === null)
393
+ return;
394
+ console.error(message);
395
+ process.exit(1);
396
+ }
397
+ const MIGRATION_COMMANDS = ['diff', 'generate', 'apply', 'rollback', 'status'];
398
+ async function main() {
399
+ const command = process.argv[2] || 'diff';
400
+ const args = process.argv.slice(3);
401
+ // Checked after the subcommand is known, so a typo gets the usage line instead of
402
+ // a warning about the wrong database.
403
+ if (!MIGRATION_COMMANDS.includes(command)) {
404
+ console.log('Usage: npx tsx migrate.ts [diff|generate|apply [--preview]|rollback [--preview] [steps|--to file]|status]');
405
+ process.exit(1);
406
+ }
407
+ // Every subcommand reads sys.* and writes T-SQL, so a non-SQL Server connection
408
+ // string is a mistake worth naming before any of it runs.
409
+ refuseIfNotSqlServer((0, config_1.providerFromConfig)(config, process.env), process.argv[2] ? `db:migrate:${command}` : 'db:migrate');
410
+ switch (command) {
411
+ case 'diff':
412
+ await cmdDiff();
413
+ break;
414
+ case 'generate':
415
+ await cmdGenerate();
416
+ break;
417
+ case 'apply':
418
+ await cmdApply(args);
419
+ break;
420
+ case 'rollback':
421
+ await cmdRollback(args);
422
+ break;
423
+ case 'status':
424
+ await cmdStatus();
425
+ break;
426
+ }
427
+ process.exit(0);
428
+ }
429
+ if (require.main === module) {
430
+ main().catch((err) => {
431
+ // A field type error carries one line per offending field; the message alone
432
+ // would name the provider without saying which fields to fix.
433
+ if (err instanceof field_types_1.FieldTypeError) {
434
+ console.error(`❌ ${err.message}:`);
435
+ console.error((0, config_1.formatIssues)(err.issues));
436
+ }
437
+ else {
438
+ console.error(`❌ Migration failed: ${err instanceof Error ? err.message : err}`);
439
+ }
440
+ process.exit(1);
441
+ });
442
+ }
@@ -26,6 +26,10 @@ export interface SchemaModel {
26
26
  fields: SchemaField[];
27
27
  compoundUniques: Array<string[] | SchemaIndexDefinition>;
28
28
  indexes: Array<string[] | SchemaIndexDefinition>;
29
+ /** A `view` block rather than a table. */
30
+ isView?: boolean;
31
+ /** The `@@sql("...")` body; required for a view. */
32
+ viewSql?: string;
29
33
  }
30
34
  export interface DbColumn {
31
35
  columnName: string;
@@ -39,11 +43,13 @@ export interface DbColumn {
39
43
  defaultValue?: string;
40
44
  }
41
45
  export interface MigrationOp {
42
- type: 'CREATE_TABLE' | 'ADD_COLUMN' | 'DROP_COLUMN' | 'ALTER_COLUMN' | 'ADD_INDEX' | 'ADD_UNIQUE' | 'DROP_INDEX' | 'DROP_UNIQUE' | 'DROP_TABLE';
46
+ type: 'CREATE_TABLE' | 'ADD_COLUMN' | 'DROP_COLUMN' | 'ALTER_COLUMN' | 'ADD_INDEX' | 'ADD_UNIQUE' | 'DROP_INDEX' | 'DROP_UNIQUE' | 'DROP_TABLE' | 'CREATE_VIEW' | 'DROP_VIEW';
43
47
  table: string;
44
48
  column?: string;
45
49
  details?: string;
46
50
  sql?: string;
51
+ /** For `CREATE_VIEW`: the definition to restore on rollback. */
52
+ previousSql?: string;
47
53
  previousSqlType?: string;
48
54
  previousNullable?: boolean;
49
55
  riskWarnings?: string[];
@@ -61,7 +67,23 @@ export interface MigrationSections {
61
67
  }
62
68
  export interface AppliedMigrationRef {
63
69
  id: string;
70
+ checksum?: string;
64
71
  }
72
+ export interface AppliedMigrationDrift {
73
+ /** Applied migration files whose contents no longer match the recorded checksum. */
74
+ changed: string[];
75
+ /** Applied migration files that are no longer on disk. */
76
+ missing: string[];
77
+ }
78
+ /**
79
+ * Compares applied migrations with the files they were applied from.
80
+ *
81
+ * `apply` used to look only at the ids, so editing an applied file was invisible
82
+ * and a fresh database would get different SQL from the one already recorded.
83
+ * Rollback already refuses a changed checksum; this catches it before the next
84
+ * migration is applied, where it can still be fixed without a partial history.
85
+ */
86
+ export declare function verifyAppliedMigrations(applied: readonly AppliedMigrationRef[], readMigration: (id: string) => string | null, checksum: (content: string) => string): AppliedMigrationDrift;
65
87
  export interface MigrationCommandOptions {
66
88
  preview: boolean;
67
89
  rest: string[];
@@ -87,9 +109,17 @@ export declare function buildUniqueConstraintPreflightSql(tableName: string, fie
87
109
  */
88
110
  export declare function parseSchemaText(text: string, provider?: Provider): SchemaModel[];
89
111
  export declare function buildCreateTableSql(model: SchemaModel): string;
112
+ /**
113
+ * The migration DDL for a view.
114
+ *
115
+ * `CREATE OR ALTER` is SQL Server's form; `db:migrate` is SQL Server-only today.
116
+ * The body is expected to be a single `SELECT` (`@@sql`), so no batch splitting
117
+ * is needed here — the caller isolates the statement with `GO`.
118
+ */
119
+ export declare function buildCreateViewSql(model: SchemaModel): string;
90
120
  export declare function buildIndexDiff(model: SchemaModel, artifacts: TableArtifacts, ops: MigrationOp[]): void;
91
121
  export declare function generateColumnDiff(model: SchemaModel, dbColumns: DbColumn[], ops: MigrationOp[]): void;
92
- export declare function generateDiff(schemaModels: SchemaModel[], dbTables: string[], introspectTable: (tableName: string) => Promise<DbColumn[]>, introspectArtifacts?: (tableName: string) => Promise<TableArtifacts>): Promise<MigrationOp[]>;
122
+ export declare function generateDiff(schemaModels: SchemaModel[], dbTables: string[], introspectTable: (tableName: string) => Promise<DbColumn[]>, introspectArtifacts?: (tableName: string) => Promise<TableArtifacts>, dbViews?: string[], introspectViewDefinition?: (viewName: string) => Promise<string | null>): Promise<MigrationOp[]>;
93
123
  export declare function buildMigrationFile(timestamp: string, ops: MigrationOp[]): string;
94
124
  export declare function buildDownMigrationSql(ops: MigrationOp[]): string;
95
125
  export declare function parseMigrationSections(sql: string): MigrationSections;
@@ -99,4 +129,3 @@ export declare function parseRollbackSelection(args: string[], applied: AppliedM
99
129
  label: string;
100
130
  };
101
131
  export declare function parseMigrationCommandOptions(args: string[]): MigrationCommandOptions;
102
- //# sourceMappingURL=migration-core.d.ts.map