@an5/orm 1.3.2 → 1.3.4
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.
- package/README.md +74 -11
- package/dist/cleanup.d.ts +1 -0
- package/dist/cleanup.js +177 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/generator/src/attributes.d.ts +57 -0
- package/dist/generator/src/attributes.js +225 -0
- package/dist/generator/src/code-generator.js +49 -16
- package/dist/generator/src/config.d.ts +2 -2
- package/dist/generator/src/config.js +4 -3
- package/dist/generator/src/dialect.d.ts +52 -1
- package/dist/generator/src/dialect.js +136 -3
- package/dist/generator/src/dotnet-generator.js +67 -23
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +86 -21
- package/dist/generator/src/java-generator.d.ts +8 -0
- package/dist/generator/src/java-generator.js +108 -3
- package/dist/generator/src/kotlin-generator.js +51 -0
- package/dist/generator/src/parser.d.ts +1 -0
- package/dist/generator/src/parser.js +90 -35
- package/dist/generator/src/pull-schema.d.ts +54 -0
- package/dist/generator/src/pull-schema.js +116 -0
- package/dist/generator/src/push-apply.d.ts +4 -2
- package/dist/generator/src/push-apply.js +53 -4
- package/dist/generator/src/push-schema.d.ts +21 -9
- package/dist/generator/src/push-schema.js +132 -32
- package/dist/generator/src/python-generator.js +35 -22
- package/dist/generator/src/rust-generator.js +79 -74
- package/dist/generator/src/swift-generator.js +54 -3
- package/dist/generator/src/types.d.ts +10 -0
- package/dist/generator/src/view-order.d.ts +33 -0
- package/dist/generator/src/view-order.js +123 -0
- package/dist/migrate.d.ts +12 -0
- package/dist/migrate.js +442 -0
- package/dist/migration-core.d.ts +32 -3
- package/dist/migration-core.js +213 -43
- package/dist/provider-support.d.ts +0 -1
- package/dist/provider-support.js +0 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.js +425 -0
- package/dist/push.d.ts +1 -0
- package/dist/push.js +93 -0
- package/dist/seed.d.ts +1 -0
- package/dist/seed.js +33 -0
- package/generator/src/attributes.ts +231 -0
- package/generator/src/code-generator.ts +47 -16
- package/generator/src/config.ts +5 -3
- package/generator/src/dialect.ts +188 -5
- package/generator/src/dotnet-generator.ts +68 -24
- package/generator/src/golang-generator.ts +89 -22
- package/generator/src/java-generator.ts +109 -3
- package/generator/src/kotlin-generator.ts +51 -0
- package/generator/src/parser.ts +102 -30
- package/generator/src/pull-schema.ts +127 -0
- package/generator/src/push-apply.ts +62 -5
- package/generator/src/push-schema.ts +160 -32
- package/generator/src/python-generator.ts +33 -20
- package/generator/src/rust-generator.ts +79 -73
- package/generator/src/swift-generator.ts +55 -4
- package/generator/src/types.ts +10 -0
- package/generator/src/view-order.ts +133 -0
- package/package.json +19 -14
package/dist/migrate.js
ADDED
|
@@ -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
|
+
}
|
package/dist/migration-core.d.ts
CHANGED
|
@@ -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
|