migraguard 0.7.0 → 0.8.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.
- package/dist/cli.js +564 -134
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +22 -2
- package/dist/index.js +419 -56
- package/dist/index.js.map +1 -1
- package/package.json +11 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFile } from 'child_process';
|
|
1
|
+
import { execFile, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
+
import { readFile, mkdir, writeFile, rm, unlink, readdir, stat, copyFile } from 'fs/promises';
|
|
3
4
|
import { Command } from 'commander';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import { createRequire } from 'module';
|
|
6
7
|
import pg from 'pg';
|
|
7
|
-
import { readFile, mkdir, writeFile, rm, unlink, readdir, stat } from 'fs/promises';
|
|
8
8
|
import { dirname, resolve, join } from 'path';
|
|
9
9
|
import { existsSync } from 'fs';
|
|
10
10
|
import { randomBytes, createHash } from 'crypto';
|
|
@@ -23,13 +23,6 @@ var __export = (target, all) => {
|
|
|
23
23
|
for (var name in all)
|
|
24
24
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
25
25
|
};
|
|
26
|
-
|
|
27
|
-
// src/psql.ts
|
|
28
|
-
var psql_exports = {};
|
|
29
|
-
__export(psql_exports, {
|
|
30
|
-
executePsqlFile: () => executePsqlFile,
|
|
31
|
-
isPsqlAvailable: () => isPsqlAvailable
|
|
32
|
-
});
|
|
33
26
|
function buildPsqlEnv(config) {
|
|
34
27
|
const env = { ...process.env };
|
|
35
28
|
env["PGHOST"] = config.connection.host;
|
|
@@ -59,23 +52,300 @@ async function executePsqlFile(config, filePath) {
|
|
|
59
52
|
};
|
|
60
53
|
}
|
|
61
54
|
}
|
|
62
|
-
async function isPsqlAvailable() {
|
|
63
|
-
try {
|
|
64
|
-
await execFileAsync("psql", ["--version"]);
|
|
65
|
-
return true;
|
|
66
|
-
} catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
55
|
var execFileAsync;
|
|
71
56
|
var init_psql = __esm({
|
|
72
57
|
"src/psql.ts"() {
|
|
73
58
|
execFileAsync = promisify(execFile);
|
|
74
59
|
}
|
|
75
60
|
});
|
|
76
|
-
|
|
61
|
+
|
|
62
|
+
// src/executor.ts
|
|
63
|
+
var executor_exports = {};
|
|
64
|
+
__export(executor_exports, {
|
|
65
|
+
executeSqlFile: () => executeSqlFile,
|
|
66
|
+
spawnWithStdin: () => spawnWithStdin
|
|
67
|
+
});
|
|
68
|
+
async function executeSqlFile(config, filePath) {
|
|
69
|
+
switch (config.dialect) {
|
|
70
|
+
case "mysql":
|
|
71
|
+
return executeMysqlFile(config, filePath);
|
|
72
|
+
case "sqlite":
|
|
73
|
+
return executeSqliteFile(config, filePath);
|
|
74
|
+
default:
|
|
75
|
+
return executePsqlFile(config, filePath);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function executeMysqlFile(config, filePath) {
|
|
79
|
+
const args = [
|
|
80
|
+
`--host=${config.connection.host}`,
|
|
81
|
+
`--port=${config.connection.port}`,
|
|
82
|
+
`--user=${config.connection.user}`,
|
|
83
|
+
`--database=${config.connection.database}`,
|
|
84
|
+
"--batch"
|
|
85
|
+
];
|
|
86
|
+
const env = { ...process.env };
|
|
87
|
+
if (config.connection.password) {
|
|
88
|
+
env["MYSQL_PWD"] = config.connection.password;
|
|
89
|
+
}
|
|
90
|
+
const sql = await readFile(filePath, "utf-8");
|
|
91
|
+
return spawnWithStdin("mysql", args, sql, env);
|
|
92
|
+
}
|
|
93
|
+
async function executeSqliteFile(config, filePath) {
|
|
94
|
+
const sql = await readFile(filePath, "utf-8");
|
|
95
|
+
return spawnWithStdin("sqlite3", ["-bail", config.connection.database], sql);
|
|
96
|
+
}
|
|
97
|
+
function spawnWithStdin(cmd, args, input, env) {
|
|
98
|
+
return new Promise((resolve4) => {
|
|
99
|
+
const proc = spawn(cmd, args, {
|
|
100
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
101
|
+
env: env ?? process.env
|
|
102
|
+
});
|
|
103
|
+
let stdout = "";
|
|
104
|
+
let stderr = "";
|
|
105
|
+
proc.stdout.on("data", (chunk) => {
|
|
106
|
+
stdout += chunk.toString();
|
|
107
|
+
});
|
|
108
|
+
proc.stderr.on("data", (chunk) => {
|
|
109
|
+
stderr += chunk.toString();
|
|
110
|
+
});
|
|
111
|
+
proc.on("error", (err) => {
|
|
112
|
+
resolve4({ success: false, stdout, stderr: err.message });
|
|
113
|
+
});
|
|
114
|
+
proc.on("close", (code) => {
|
|
115
|
+
resolve4({ success: code === 0, stdout, stderr });
|
|
116
|
+
});
|
|
117
|
+
proc.stdin.write(input);
|
|
118
|
+
proc.stdin.end();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
var init_executor = __esm({
|
|
122
|
+
"src/executor.ts"() {
|
|
123
|
+
init_psql();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/db-mysql.ts
|
|
77
128
|
var ADVISORY_LOCK_KEY = "migraguard-apply";
|
|
78
129
|
var CREATE_TABLE_SQL = `
|
|
130
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
131
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
132
|
+
file_name VARCHAR(256) NOT NULL,
|
|
133
|
+
checksum VARCHAR(64) NOT NULL,
|
|
134
|
+
status VARCHAR(16) NOT NULL DEFAULT 'applied',
|
|
135
|
+
applied_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
|
136
|
+
resolved_at TIMESTAMP(6) NULL,
|
|
137
|
+
migration_class VARCHAR(16) DEFAULT 'safe',
|
|
138
|
+
phase VARCHAR(16) NULL,
|
|
139
|
+
group_name VARCHAR(256) NULL
|
|
140
|
+
) ENGINE=InnoDB;
|
|
141
|
+
`;
|
|
142
|
+
var MigraguardDbMysql = class {
|
|
143
|
+
config;
|
|
144
|
+
connection = null;
|
|
145
|
+
constructor(config) {
|
|
146
|
+
this.config = config;
|
|
147
|
+
}
|
|
148
|
+
async connect() {
|
|
149
|
+
let mysql;
|
|
150
|
+
try {
|
|
151
|
+
const id = "mysql2/promise";
|
|
152
|
+
const mod = await import(
|
|
153
|
+
/* @vite-ignore */
|
|
154
|
+
id
|
|
155
|
+
);
|
|
156
|
+
mysql = mod.default;
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"mysql2 is required for MySQL dialect. Install it: npm install mysql2"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
this.connection = await mysql.createConnection({
|
|
163
|
+
host: this.config.connection.host,
|
|
164
|
+
port: this.config.connection.port,
|
|
165
|
+
database: this.config.connection.database,
|
|
166
|
+
user: this.config.connection.user,
|
|
167
|
+
password: this.config.connection.password
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async close() {
|
|
171
|
+
await this.connection?.end();
|
|
172
|
+
}
|
|
173
|
+
async ensureTable() {
|
|
174
|
+
await this.exec(CREATE_TABLE_SQL);
|
|
175
|
+
}
|
|
176
|
+
async acquireAdvisoryLock() {
|
|
177
|
+
await this.exec(`SELECT GET_LOCK(?, -1)`, [ADVISORY_LOCK_KEY]);
|
|
178
|
+
}
|
|
179
|
+
async releaseAdvisoryLock() {
|
|
180
|
+
await this.exec(`SELECT RELEASE_LOCK(?)`, [ADVISORY_LOCK_KEY]);
|
|
181
|
+
}
|
|
182
|
+
async getAllRecords() {
|
|
183
|
+
const rows = await this.queryRows(
|
|
184
|
+
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
185
|
+
migration_class, phase, group_name
|
|
186
|
+
FROM schema_migrations
|
|
187
|
+
ORDER BY applied_at ASC`
|
|
188
|
+
);
|
|
189
|
+
return rows.map(mapRow);
|
|
190
|
+
}
|
|
191
|
+
async getRecordsForFile(fileName) {
|
|
192
|
+
const rows = await this.queryRows(
|
|
193
|
+
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
194
|
+
migration_class, phase, group_name
|
|
195
|
+
FROM schema_migrations
|
|
196
|
+
WHERE file_name = ?
|
|
197
|
+
ORDER BY applied_at ASC`,
|
|
198
|
+
[fileName]
|
|
199
|
+
);
|
|
200
|
+
return rows.map(mapRow);
|
|
201
|
+
}
|
|
202
|
+
async insertRecord(fileName, checksum, status, options) {
|
|
203
|
+
const migrationClass = options?.migrationClass ?? "safe";
|
|
204
|
+
const phase = options?.phase ?? null;
|
|
205
|
+
const groupName = options?.groupName ?? null;
|
|
206
|
+
if (status === "skipped") {
|
|
207
|
+
await this.exec(
|
|
208
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name)
|
|
209
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP(6), ?, ?, ?)`,
|
|
210
|
+
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
211
|
+
);
|
|
212
|
+
} else {
|
|
213
|
+
await this.exec(
|
|
214
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
216
|
+
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
conn() {
|
|
221
|
+
if (!this.connection) throw new Error("Not connected");
|
|
222
|
+
return this.connection;
|
|
223
|
+
}
|
|
224
|
+
async exec(sql, values) {
|
|
225
|
+
await this.conn().execute(sql, values);
|
|
226
|
+
}
|
|
227
|
+
async queryRows(sql, values) {
|
|
228
|
+
const [rows] = await this.conn().execute(sql, values);
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
function mapRow(row) {
|
|
233
|
+
return {
|
|
234
|
+
fileName: row["file_name"],
|
|
235
|
+
checksum: row["checksum"],
|
|
236
|
+
status: row["status"],
|
|
237
|
+
appliedAt: row["applied_at"] instanceof Date ? row["applied_at"] : new Date(row["applied_at"]),
|
|
238
|
+
resolvedAt: row["resolved_at"] ? row["resolved_at"] instanceof Date ? row["resolved_at"] : new Date(row["resolved_at"]) : null,
|
|
239
|
+
migrationClass: row["migration_class"] ?? "safe",
|
|
240
|
+
phase: row["phase"] ?? null,
|
|
241
|
+
groupName: row["group_name"] ?? null
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/db-sqlite.ts
|
|
246
|
+
var CREATE_TABLE_SQL2 = `
|
|
247
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
248
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
249
|
+
file_name TEXT NOT NULL,
|
|
250
|
+
checksum TEXT NOT NULL,
|
|
251
|
+
status TEXT NOT NULL DEFAULT 'applied',
|
|
252
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
253
|
+
resolved_at TEXT,
|
|
254
|
+
migration_class TEXT DEFAULT 'safe',
|
|
255
|
+
phase TEXT,
|
|
256
|
+
group_name TEXT
|
|
257
|
+
);
|
|
258
|
+
`;
|
|
259
|
+
var MigraguardDbSqlite = class {
|
|
260
|
+
config;
|
|
261
|
+
database = null;
|
|
262
|
+
constructor(config) {
|
|
263
|
+
this.config = config;
|
|
264
|
+
}
|
|
265
|
+
async connect() {
|
|
266
|
+
let Database;
|
|
267
|
+
try {
|
|
268
|
+
const id = "better-sqlite3";
|
|
269
|
+
const mod = await import(
|
|
270
|
+
/* @vite-ignore */
|
|
271
|
+
id
|
|
272
|
+
);
|
|
273
|
+
Database = mod.default;
|
|
274
|
+
} catch {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"better-sqlite3 is required for SQLite dialect. Install it: npm install better-sqlite3"
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
this.database = new Database(this.config.connection.database);
|
|
280
|
+
this.db().pragma("journal_mode = WAL");
|
|
281
|
+
}
|
|
282
|
+
async close() {
|
|
283
|
+
this.database?.close();
|
|
284
|
+
}
|
|
285
|
+
async ensureTable() {
|
|
286
|
+
this.db().exec(CREATE_TABLE_SQL2);
|
|
287
|
+
}
|
|
288
|
+
async acquireAdvisoryLock() {
|
|
289
|
+
}
|
|
290
|
+
async releaseAdvisoryLock() {
|
|
291
|
+
}
|
|
292
|
+
async getAllRecords() {
|
|
293
|
+
const rows = this.db().prepare(
|
|
294
|
+
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
295
|
+
migration_class, phase, group_name
|
|
296
|
+
FROM schema_migrations
|
|
297
|
+
ORDER BY applied_at ASC`
|
|
298
|
+
).all();
|
|
299
|
+
return rows.map(mapRow2);
|
|
300
|
+
}
|
|
301
|
+
async getRecordsForFile(fileName) {
|
|
302
|
+
const rows = this.db().prepare(
|
|
303
|
+
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
304
|
+
migration_class, phase, group_name
|
|
305
|
+
FROM schema_migrations
|
|
306
|
+
WHERE file_name = ?
|
|
307
|
+
ORDER BY applied_at ASC`
|
|
308
|
+
).all(fileName);
|
|
309
|
+
return rows.map(mapRow2);
|
|
310
|
+
}
|
|
311
|
+
async insertRecord(fileName, checksum, status, options) {
|
|
312
|
+
const migrationClass = options?.migrationClass ?? "safe";
|
|
313
|
+
const phase = options?.phase ?? null;
|
|
314
|
+
const groupName = options?.groupName ?? null;
|
|
315
|
+
if (status === "skipped") {
|
|
316
|
+
this.db().prepare(
|
|
317
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name)
|
|
318
|
+
VALUES (?, ?, ?, datetime('now'), ?, ?, ?)`
|
|
319
|
+
).run(fileName, checksum, status, migrationClass, phase, groupName);
|
|
320
|
+
} else {
|
|
321
|
+
this.db().prepare(
|
|
322
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name)
|
|
323
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
324
|
+
).run(fileName, checksum, status, migrationClass, phase, groupName);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
db() {
|
|
328
|
+
if (!this.database) throw new Error("Not connected");
|
|
329
|
+
return this.database;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
function mapRow2(row) {
|
|
333
|
+
return {
|
|
334
|
+
fileName: row["file_name"],
|
|
335
|
+
checksum: row["checksum"],
|
|
336
|
+
status: row["status"],
|
|
337
|
+
appliedAt: new Date(row["applied_at"]),
|
|
338
|
+
resolvedAt: row["resolved_at"] ? new Date(row["resolved_at"]) : null,
|
|
339
|
+
migrationClass: row["migration_class"] ?? "safe",
|
|
340
|
+
phase: row["phase"] ?? null,
|
|
341
|
+
groupName: row["group_name"] ?? null
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/db.ts
|
|
346
|
+
var { Client } = pg;
|
|
347
|
+
var ADVISORY_LOCK_KEY2 = "migraguard-apply";
|
|
348
|
+
var CREATE_TABLE_SQL3 = `
|
|
79
349
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
80
350
|
id BIGSERIAL PRIMARY KEY,
|
|
81
351
|
file_name VARCHAR(256) NOT NULL,
|
|
@@ -91,6 +361,16 @@ ALTER TABLE schema_migrations
|
|
|
91
361
|
ADD COLUMN IF NOT EXISTS phase VARCHAR(16),
|
|
92
362
|
ADD COLUMN IF NOT EXISTS group_name VARCHAR(256);
|
|
93
363
|
`;
|
|
364
|
+
function createDb(config) {
|
|
365
|
+
switch (config.dialect) {
|
|
366
|
+
case "mysql":
|
|
367
|
+
return new MigraguardDbMysql(config);
|
|
368
|
+
case "sqlite":
|
|
369
|
+
return new MigraguardDbSqlite(config);
|
|
370
|
+
default:
|
|
371
|
+
return new MigraguardDb(config);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
94
374
|
var MigraguardDb = class {
|
|
95
375
|
client;
|
|
96
376
|
constructor(config) {
|
|
@@ -109,14 +389,14 @@ var MigraguardDb = class {
|
|
|
109
389
|
await this.client.end();
|
|
110
390
|
}
|
|
111
391
|
async ensureTable() {
|
|
112
|
-
await this.client.query(
|
|
392
|
+
await this.client.query(CREATE_TABLE_SQL3);
|
|
113
393
|
await this.client.query(ALTER_TABLE_SQL);
|
|
114
394
|
}
|
|
115
395
|
async acquireAdvisoryLock() {
|
|
116
|
-
await this.client.query(`SELECT pg_advisory_lock(hashtext($1))`, [
|
|
396
|
+
await this.client.query(`SELECT pg_advisory_lock(hashtext($1))`, [ADVISORY_LOCK_KEY2]);
|
|
117
397
|
}
|
|
118
398
|
async releaseAdvisoryLock() {
|
|
119
|
-
await this.client.query(`SELECT pg_advisory_unlock(hashtext($1))`, [
|
|
399
|
+
await this.client.query(`SELECT pg_advisory_unlock(hashtext($1))`, [ADVISORY_LOCK_KEY2]);
|
|
120
400
|
}
|
|
121
401
|
async getAllRecords() {
|
|
122
402
|
const result = await this.client.query(
|
|
@@ -125,7 +405,7 @@ var MigraguardDb = class {
|
|
|
125
405
|
FROM schema_migrations
|
|
126
406
|
ORDER BY applied_at ASC`
|
|
127
407
|
);
|
|
128
|
-
return result.rows.map(
|
|
408
|
+
return result.rows.map(mapRow3);
|
|
129
409
|
}
|
|
130
410
|
async getRecordsForFile(fileName) {
|
|
131
411
|
const result = await this.client.query(
|
|
@@ -136,7 +416,7 @@ var MigraguardDb = class {
|
|
|
136
416
|
ORDER BY applied_at ASC`,
|
|
137
417
|
[fileName]
|
|
138
418
|
);
|
|
139
|
-
return result.rows.map(
|
|
419
|
+
return result.rows.map(mapRow3);
|
|
140
420
|
}
|
|
141
421
|
async insertRecord(fileName, checksum, status, options) {
|
|
142
422
|
const migrationClass = options?.migrationClass ?? "safe";
|
|
@@ -160,7 +440,7 @@ var MigraguardDb = class {
|
|
|
160
440
|
return this.client;
|
|
161
441
|
}
|
|
162
442
|
};
|
|
163
|
-
function
|
|
443
|
+
function mapRow3(row) {
|
|
164
444
|
return {
|
|
165
445
|
fileName: row["file_name"],
|
|
166
446
|
checksum: row["checksum"],
|
|
@@ -172,6 +452,9 @@ function mapRow(row) {
|
|
|
172
452
|
groupName: row["group_name"] ?? null
|
|
173
453
|
};
|
|
174
454
|
}
|
|
455
|
+
|
|
456
|
+
// src/index.ts
|
|
457
|
+
init_executor();
|
|
175
458
|
var CONFIG_FILE_NAME = "migraguard.config.json";
|
|
176
459
|
var DEFAULT_NAMING = {
|
|
177
460
|
pattern: "{timestamp}__{description}.sql",
|
|
@@ -179,12 +462,16 @@ var DEFAULT_NAMING = {
|
|
|
179
462
|
prefix: "",
|
|
180
463
|
sortKey: "timestamp"
|
|
181
464
|
};
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
};
|
|
465
|
+
function getDefaultConnection(dialect) {
|
|
466
|
+
switch (dialect) {
|
|
467
|
+
case "mysql":
|
|
468
|
+
return { host: "localhost", port: 3306, database: "mysql", user: "root" };
|
|
469
|
+
case "sqlite":
|
|
470
|
+
return { host: "", port: 0, database: "./database.sqlite3", user: "" };
|
|
471
|
+
default:
|
|
472
|
+
return { host: "localhost", port: 5432, database: "postgres", user: "postgres" };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
188
475
|
var DEFAULT_DUMP = {
|
|
189
476
|
normalize: true,
|
|
190
477
|
excludeOwners: true,
|
|
@@ -251,14 +538,30 @@ function getDefaultLintRules(dialect) {
|
|
|
251
538
|
var DEFAULT_LINT = {
|
|
252
539
|
rules: DEFAULT_PG_LINT_RULES
|
|
253
540
|
};
|
|
254
|
-
function applyEnvOverrides(connection) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
541
|
+
function applyEnvOverrides(connection, dialect) {
|
|
542
|
+
switch (dialect) {
|
|
543
|
+
case "mysql":
|
|
544
|
+
return {
|
|
545
|
+
host: process.env["MYSQL_HOST"] ?? connection.host,
|
|
546
|
+
port: process.env["MYSQL_TCP_PORT"] ? parseInt(process.env["MYSQL_TCP_PORT"], 10) : connection.port,
|
|
547
|
+
database: process.env["MYSQL_DATABASE"] ?? connection.database,
|
|
548
|
+
user: process.env["MYSQL_USER"] ?? connection.user,
|
|
549
|
+
password: process.env["MYSQL_PWD"] ?? connection.password
|
|
550
|
+
};
|
|
551
|
+
case "sqlite":
|
|
552
|
+
return {
|
|
553
|
+
...connection,
|
|
554
|
+
database: process.env["SQLITE_DATABASE"] ?? connection.database
|
|
555
|
+
};
|
|
556
|
+
default:
|
|
557
|
+
return {
|
|
558
|
+
host: process.env["PGHOST"] ?? connection.host,
|
|
559
|
+
port: process.env["PGPORT"] ? parseInt(process.env["PGPORT"], 10) : connection.port,
|
|
560
|
+
database: process.env["PGDATABASE"] ?? connection.database,
|
|
561
|
+
user: process.env["PGUSER"] ?? connection.user,
|
|
562
|
+
password: process.env["PGPASSWORD"] ?? connection.password
|
|
563
|
+
};
|
|
564
|
+
}
|
|
262
565
|
}
|
|
263
566
|
function resolveMigrationsDirs(raw) {
|
|
264
567
|
if (raw.migrationsDirs && raw.migrationsDirs.length > 0) {
|
|
@@ -287,7 +590,7 @@ function findConfigFile(startDir) {
|
|
|
287
590
|
function buildConfig(raw, configDir) {
|
|
288
591
|
const dialect = raw.dialect ?? "postgresql";
|
|
289
592
|
const connection = {
|
|
290
|
-
...
|
|
593
|
+
...getDefaultConnection(dialect),
|
|
291
594
|
...raw.connection
|
|
292
595
|
};
|
|
293
596
|
const defaultRules = getDefaultLintRules(dialect);
|
|
@@ -299,7 +602,7 @@ function buildConfig(raw, configDir) {
|
|
|
299
602
|
schemaFile: raw.schemaFile ?? "db/schema.sql",
|
|
300
603
|
metadataFile: raw.metadataFile ?? "db/.migraguard/metadata.json",
|
|
301
604
|
naming: { ...DEFAULT_NAMING, ...raw.naming },
|
|
302
|
-
connection: applyEnvOverrides(connection),
|
|
605
|
+
connection: applyEnvOverrides(connection, dialect),
|
|
303
606
|
dump: { ...DEFAULT_DUMP, ...raw.dump },
|
|
304
607
|
lint: {
|
|
305
608
|
...DEFAULT_LINT,
|
|
@@ -663,7 +966,7 @@ async function checksumFile(filePath) {
|
|
|
663
966
|
}
|
|
664
967
|
|
|
665
968
|
// src/commands/apply.ts
|
|
666
|
-
|
|
969
|
+
init_executor();
|
|
667
970
|
var execFileAsync2 = promisify(execFile);
|
|
668
971
|
function buildPgDumpEnv(config) {
|
|
669
972
|
const env = { ...process.env };
|
|
@@ -677,6 +980,16 @@ function buildPgDumpEnv(config) {
|
|
|
677
980
|
return env;
|
|
678
981
|
}
|
|
679
982
|
async function dumpSchema(config) {
|
|
983
|
+
switch (config.dialect) {
|
|
984
|
+
case "mysql":
|
|
985
|
+
return dumpMysqlSchema(config);
|
|
986
|
+
case "sqlite":
|
|
987
|
+
return dumpSqliteSchema(config);
|
|
988
|
+
default:
|
|
989
|
+
return dumpPgSchema(config);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async function dumpPgSchema(config) {
|
|
680
993
|
const pgDumpCmd = config.dump.pgDumpCommand;
|
|
681
994
|
const dumpArgs = ["--schema-only"];
|
|
682
995
|
if (config.dump.excludeOwners) dumpArgs.push("--no-owner");
|
|
@@ -696,6 +1009,35 @@ async function dumpSchema(config) {
|
|
|
696
1009
|
}
|
|
697
1010
|
return stdout;
|
|
698
1011
|
}
|
|
1012
|
+
async function dumpMysqlSchema(config) {
|
|
1013
|
+
const args = [
|
|
1014
|
+
"--no-data",
|
|
1015
|
+
"--skip-comments",
|
|
1016
|
+
`--host=${config.connection.host}`,
|
|
1017
|
+
`--port=${config.connection.port}`,
|
|
1018
|
+
`--user=${config.connection.user}`,
|
|
1019
|
+
config.connection.database
|
|
1020
|
+
];
|
|
1021
|
+
const env = { ...process.env };
|
|
1022
|
+
if (config.connection.password) {
|
|
1023
|
+
env["MYSQL_PWD"] = config.connection.password;
|
|
1024
|
+
}
|
|
1025
|
+
const { stdout } = await execFileAsync2("mysqldump", args, { env });
|
|
1026
|
+
if (config.dump.normalize) {
|
|
1027
|
+
return normalizeMysqlSchema(stdout);
|
|
1028
|
+
}
|
|
1029
|
+
return stdout;
|
|
1030
|
+
}
|
|
1031
|
+
async function dumpSqliteSchema(config) {
|
|
1032
|
+
const { stdout } = await execFileAsync2("sqlite3", [
|
|
1033
|
+
config.connection.database,
|
|
1034
|
+
".schema"
|
|
1035
|
+
]);
|
|
1036
|
+
if (config.dump.normalize) {
|
|
1037
|
+
return normalizeSqliteSchema(stdout);
|
|
1038
|
+
}
|
|
1039
|
+
return stdout;
|
|
1040
|
+
}
|
|
699
1041
|
function normalizeSchema(raw) {
|
|
700
1042
|
const lines = raw.split("\n");
|
|
701
1043
|
const filtered = lines.filter((line) => {
|
|
@@ -707,9 +1049,30 @@ function normalizeSchema(raw) {
|
|
|
707
1049
|
if (line.startsWith("\\unrestrict")) return false;
|
|
708
1050
|
return true;
|
|
709
1051
|
});
|
|
1052
|
+
return collapseAndTrim(filtered);
|
|
1053
|
+
}
|
|
1054
|
+
function normalizeMysqlSchema(raw) {
|
|
1055
|
+
const lines = raw.split("\n");
|
|
1056
|
+
const filtered = lines.filter((line) => {
|
|
1057
|
+
if (line.startsWith("--")) return false;
|
|
1058
|
+
if (line.startsWith("/*")) return false;
|
|
1059
|
+
if (line.startsWith("/*!")) return false;
|
|
1060
|
+
if (line.startsWith("SET ")) return false;
|
|
1061
|
+
if (line.startsWith("LOCK TABLES")) return false;
|
|
1062
|
+
if (line.startsWith("UNLOCK TABLES")) return false;
|
|
1063
|
+
return true;
|
|
1064
|
+
});
|
|
1065
|
+
return collapseAndTrim(filtered);
|
|
1066
|
+
}
|
|
1067
|
+
function normalizeSqliteSchema(raw) {
|
|
1068
|
+
const lines = raw.split("\n");
|
|
1069
|
+
const filtered = lines.filter((line) => !line.startsWith("--"));
|
|
1070
|
+
return collapseAndTrim(filtered);
|
|
1071
|
+
}
|
|
1072
|
+
function collapseAndTrim(lines) {
|
|
710
1073
|
const result = [];
|
|
711
1074
|
let prevBlank = false;
|
|
712
|
-
for (const line of
|
|
1075
|
+
for (const line of lines) {
|
|
713
1076
|
const isBlank = line.trim() === "";
|
|
714
1077
|
if (isBlank && prevBlank) continue;
|
|
715
1078
|
result.push(line);
|
|
@@ -1417,7 +1780,7 @@ async function commandApply(config, options) {
|
|
|
1417
1780
|
graph = await buildDependencyGraph(config);
|
|
1418
1781
|
leafSet = new Set(findLeafNodes(graph));
|
|
1419
1782
|
}
|
|
1420
|
-
const db =
|
|
1783
|
+
const db = createDb(config);
|
|
1421
1784
|
try {
|
|
1422
1785
|
await db.connect();
|
|
1423
1786
|
await db.ensureTable();
|
|
@@ -1540,7 +1903,7 @@ async function commandApply(config, options) {
|
|
|
1540
1903
|
}
|
|
1541
1904
|
async function processFile(config, db, filePath, fileName, fileRecords, latestRecord, currentChecksum, isEditable, result, insertOpts) {
|
|
1542
1905
|
if (!latestRecord) {
|
|
1543
|
-
const psqlResult = await
|
|
1906
|
+
const psqlResult = await executeSqlFile(config, filePath);
|
|
1544
1907
|
if (psqlResult.success) {
|
|
1545
1908
|
await db.insertRecord(fileName, currentChecksum, "applied", insertOpts);
|
|
1546
1909
|
result.applied.push(fileName);
|
|
@@ -1563,7 +1926,7 @@ async function processFile(config, db, filePath, fileName, fileRecords, latestRe
|
|
|
1563
1926
|
if (latestRecord.status === "failed") {
|
|
1564
1927
|
if (isEditable) {
|
|
1565
1928
|
console.log(chalk.yellow(` \u21BB retrying failed: ${fileName}`));
|
|
1566
|
-
const psqlResult = await
|
|
1929
|
+
const psqlResult = await executeSqlFile(config, filePath);
|
|
1567
1930
|
if (psqlResult.success) {
|
|
1568
1931
|
await db.insertRecord(fileName, currentChecksum, "applied", insertOpts);
|
|
1569
1932
|
result.applied.push(fileName);
|
|
@@ -1600,7 +1963,7 @@ async function processFile(config, db, filePath, fileName, fileRecords, latestRe
|
|
|
1600
1963
|
}
|
|
1601
1964
|
if (isEditable) {
|
|
1602
1965
|
console.log(chalk.yellow(` \u21BB re-applying (changed): ${fileName}`));
|
|
1603
|
-
const psqlResult = await
|
|
1966
|
+
const psqlResult = await executeSqlFile(config, filePath);
|
|
1604
1967
|
if (psqlResult.success) {
|
|
1605
1968
|
await db.insertRecord(fileName, currentChecksum, "applied", insertOpts);
|
|
1606
1969
|
result.applied.push(fileName);
|
|
@@ -1629,8 +1992,8 @@ async function applyFromBaseline(config, db, metadata, result) {
|
|
|
1629
1992
|
return;
|
|
1630
1993
|
}
|
|
1631
1994
|
console.log(chalk.blue("Applying baseline schema..."));
|
|
1632
|
-
const {
|
|
1633
|
-
const psqlResult = await
|
|
1995
|
+
const { executeSqlFile: execSql } = await Promise.resolve().then(() => (init_executor(), executor_exports));
|
|
1996
|
+
const psqlResult = await execSql(config, schemaPath);
|
|
1634
1997
|
if (!psqlResult.success) {
|
|
1635
1998
|
result.errors.push(`Failed to apply baseline schema: ${psqlResult.stderr}`);
|
|
1636
1999
|
console.error(chalk.red(` \u2717 baseline schema apply failed`));
|
|
@@ -1648,7 +2011,7 @@ async function applyFromBaseline(config, db, metadata, result) {
|
|
|
1648
2011
|
}
|
|
1649
2012
|
}
|
|
1650
2013
|
async function commandGroupStatus(config, groupName) {
|
|
1651
|
-
const db =
|
|
2014
|
+
const db = createDb(config);
|
|
1652
2015
|
try {
|
|
1653
2016
|
await db.connect();
|
|
1654
2017
|
await db.ensureTable();
|
|
@@ -1708,7 +2071,7 @@ var STATUS_MAP = {
|
|
|
1708
2071
|
};
|
|
1709
2072
|
async function commandAdvance(config, options) {
|
|
1710
2073
|
const { group, phase, status } = options;
|
|
1711
|
-
const db =
|
|
2074
|
+
const db = createDb(config);
|
|
1712
2075
|
try {
|
|
1713
2076
|
await db.connect();
|
|
1714
2077
|
await db.ensureTable();
|
|
@@ -1759,7 +2122,7 @@ async function commandGate(config, options) {
|
|
|
1759
2122
|
forbidden: options.forbidden ?? []
|
|
1760
2123
|
};
|
|
1761
2124
|
}
|
|
1762
|
-
const db =
|
|
2125
|
+
const db = createDb(config);
|
|
1763
2126
|
try {
|
|
1764
2127
|
await db.connect();
|
|
1765
2128
|
await db.ensureTable();
|
|
@@ -1822,10 +2185,10 @@ function matchesState(currentState, targetState) {
|
|
|
1822
2185
|
if (currentIdx === -1 || targetIdx === -1) return false;
|
|
1823
2186
|
return currentIdx >= targetIdx;
|
|
1824
2187
|
}
|
|
1825
|
-
|
|
2188
|
+
init_executor();
|
|
1826
2189
|
async function commandApplyPhase(config, options) {
|
|
1827
2190
|
const { group, phase } = options;
|
|
1828
|
-
const db =
|
|
2191
|
+
const db = createDb(config);
|
|
1829
2192
|
try {
|
|
1830
2193
|
await db.connect();
|
|
1831
2194
|
await db.ensureTable();
|
|
@@ -1847,7 +2210,7 @@ async function commandApplyPhase(config, options) {
|
|
|
1847
2210
|
return { success: false, group, phase, error: msg };
|
|
1848
2211
|
}
|
|
1849
2212
|
const checksum = await checksumFile(file.filePath);
|
|
1850
|
-
const psqlResult = await
|
|
2213
|
+
const psqlResult = await executeSqlFile(config, file.filePath);
|
|
1851
2214
|
if (psqlResult.success) {
|
|
1852
2215
|
await db.insertRecord(file.fileName, checksum, "applied", {
|
|
1853
2216
|
migrationClass: "expand_contract",
|
|
@@ -1873,7 +2236,7 @@ async function commandApplyPhase(config, options) {
|
|
|
1873
2236
|
}
|
|
1874
2237
|
}
|
|
1875
2238
|
async function commandBaseline(config, options) {
|
|
1876
|
-
const db =
|
|
2239
|
+
const db = createDb(config);
|
|
1877
2240
|
try {
|
|
1878
2241
|
await db.connect();
|
|
1879
2242
|
await db.ensureTable();
|
|
@@ -4218,7 +4581,7 @@ async function commandEditable(config) {
|
|
|
4218
4581
|
}
|
|
4219
4582
|
let dbConnected = false;
|
|
4220
4583
|
try {
|
|
4221
|
-
const db =
|
|
4584
|
+
const db = createDb(config);
|
|
4222
4585
|
await db.connect();
|
|
4223
4586
|
dbConnected = true;
|
|
4224
4587
|
try {
|
|
@@ -4270,7 +4633,7 @@ function getLatestRecord3(records) {
|
|
|
4270
4633
|
);
|
|
4271
4634
|
}
|
|
4272
4635
|
async function commandStatus(config) {
|
|
4273
|
-
const db =
|
|
4636
|
+
const db = createDb(config);
|
|
4274
4637
|
const entries = [];
|
|
4275
4638
|
let groups = [];
|
|
4276
4639
|
try {
|
|
@@ -4395,7 +4758,7 @@ function formatGroupState(state) {
|
|
|
4395
4758
|
}
|
|
4396
4759
|
}
|
|
4397
4760
|
async function commandResolve(config, fileName) {
|
|
4398
|
-
const db =
|
|
4761
|
+
const db = createDb(config);
|
|
4399
4762
|
try {
|
|
4400
4763
|
await db.connect();
|
|
4401
4764
|
await db.ensureTable();
|
|
@@ -4464,14 +4827,13 @@ async function commandDiff(config) {
|
|
|
4464
4827
|
console.error(diff);
|
|
4465
4828
|
return { identical: false, diff };
|
|
4466
4829
|
}
|
|
4467
|
-
|
|
4830
|
+
init_executor();
|
|
4468
4831
|
var { Client: Client2 } = pg;
|
|
4469
4832
|
var execFileAsync3 = promisify(execFile);
|
|
4470
|
-
function
|
|
4471
|
-
|
|
4472
|
-
return `migraguard_shadow_${suffix}`;
|
|
4833
|
+
function shadowId() {
|
|
4834
|
+
return randomBytes(4).toString("hex");
|
|
4473
4835
|
}
|
|
4474
|
-
function
|
|
4836
|
+
function buildPgEnv(conn) {
|
|
4475
4837
|
const env = { ...process.env };
|
|
4476
4838
|
env["PGHOST"] = conn.host;
|
|
4477
4839
|
env["PGPORT"] = String(conn.port);
|
|
@@ -4479,7 +4841,7 @@ function buildEnv(conn) {
|
|
|
4479
4841
|
if (conn.password) env["PGPASSWORD"] = conn.password;
|
|
4480
4842
|
return env;
|
|
4481
4843
|
}
|
|
4482
|
-
async function
|
|
4844
|
+
async function createPgShadow(conn, dbName) {
|
|
4483
4845
|
const client = new Client2({
|
|
4484
4846
|
host: conn.host,
|
|
4485
4847
|
port: conn.port,
|
|
@@ -4494,7 +4856,7 @@ async function createShadowDb(conn, dbName) {
|
|
|
4494
4856
|
await client.end();
|
|
4495
4857
|
}
|
|
4496
4858
|
}
|
|
4497
|
-
async function
|
|
4859
|
+
async function dropPgShadow(conn, dbName) {
|
|
4498
4860
|
const client = new Client2({
|
|
4499
4861
|
host: conn.host,
|
|
4500
4862
|
port: conn.port,
|
|
@@ -4509,9 +4871,9 @@ async function dropShadowDb(conn, dbName) {
|
|
|
4509
4871
|
await client.end();
|
|
4510
4872
|
}
|
|
4511
4873
|
}
|
|
4512
|
-
async function
|
|
4874
|
+
async function dumpPgSourceToShadow(config, shadowName2) {
|
|
4513
4875
|
const conn = config.connection;
|
|
4514
|
-
const env =
|
|
4876
|
+
const env = buildPgEnv(conn);
|
|
4515
4877
|
const pgDumpCmd = config.dump.pgDumpCommand;
|
|
4516
4878
|
let dumpOutput;
|
|
4517
4879
|
if (pgDumpCmd && pgDumpCmd.length > 0) {
|
|
@@ -4526,8 +4888,8 @@ async function dumpSourceToShadow(config, shadowName) {
|
|
|
4526
4888
|
const tmpFile = join(tmpdir(), `migraguard-dump-${randomBytes(4).toString("hex")}.sql`);
|
|
4527
4889
|
await writeFile(tmpFile, dumpOutput, "utf-8");
|
|
4528
4890
|
try {
|
|
4529
|
-
const restoreEnv =
|
|
4530
|
-
restoreEnv["PGDATABASE"] =
|
|
4891
|
+
const restoreEnv = buildPgEnv(conn);
|
|
4892
|
+
restoreEnv["PGDATABASE"] = shadowName2;
|
|
4531
4893
|
await execFileAsync3("psql", ["-v", "ON_ERROR_STOP=1", "-f", tmpFile], {
|
|
4532
4894
|
env: restoreEnv,
|
|
4533
4895
|
maxBuffer: 50 * 1024 * 1024
|
|
@@ -4537,117 +4899,182 @@ async function dumpSourceToShadow(config, shadowName) {
|
|
|
4537
4899
|
});
|
|
4538
4900
|
}
|
|
4539
4901
|
}
|
|
4540
|
-
function
|
|
4902
|
+
async function createMysqlShadow(config, dbName) {
|
|
4903
|
+
const args = [
|
|
4904
|
+
`--host=${config.connection.host}`,
|
|
4905
|
+
`--port=${config.connection.port}`,
|
|
4906
|
+
`--user=${config.connection.user}`
|
|
4907
|
+
];
|
|
4908
|
+
const env = { ...process.env };
|
|
4909
|
+
if (config.connection.password) env["MYSQL_PWD"] = config.connection.password;
|
|
4910
|
+
const safeName = dbName.replace(/`/g, "``");
|
|
4911
|
+
await spawnWithStdin("mysql", args, `CREATE DATABASE \`${safeName}\`;`, env);
|
|
4912
|
+
}
|
|
4913
|
+
async function dropMysqlShadow(config, dbName) {
|
|
4914
|
+
const args = [
|
|
4915
|
+
`--host=${config.connection.host}`,
|
|
4916
|
+
`--port=${config.connection.port}`,
|
|
4917
|
+
`--user=${config.connection.user}`
|
|
4918
|
+
];
|
|
4919
|
+
const env = { ...process.env };
|
|
4920
|
+
if (config.connection.password) env["MYSQL_PWD"] = config.connection.password;
|
|
4921
|
+
const safeName = dbName.replace(/`/g, "``");
|
|
4922
|
+
await spawnWithStdin("mysql", args, `DROP DATABASE IF EXISTS \`${safeName}\`;`, env);
|
|
4923
|
+
}
|
|
4924
|
+
async function dumpMysqlSourceToShadow(config, shadowName2) {
|
|
4925
|
+
const conn = config.connection;
|
|
4926
|
+
const env = { ...process.env };
|
|
4927
|
+
if (conn.password) env["MYSQL_PWD"] = conn.password;
|
|
4928
|
+
const { stdout: dumpOutput } = await execFileAsync3("mysqldump", [
|
|
4929
|
+
`--host=${conn.host}`,
|
|
4930
|
+
`--port=${conn.port}`,
|
|
4931
|
+
`--user=${conn.user}`,
|
|
4932
|
+
conn.database
|
|
4933
|
+
], { env });
|
|
4934
|
+
const restoreArgs = [
|
|
4935
|
+
`--host=${conn.host}`,
|
|
4936
|
+
`--port=${conn.port}`,
|
|
4937
|
+
`--user=${conn.user}`,
|
|
4938
|
+
`--database=${shadowName2}`
|
|
4939
|
+
];
|
|
4940
|
+
await spawnWithStdin("mysql", restoreArgs, dumpOutput, env);
|
|
4941
|
+
}
|
|
4942
|
+
async function dumpSqliteSourceToShadow(config, shadowPath) {
|
|
4943
|
+
await copyFile(config.connection.database, shadowPath);
|
|
4944
|
+
}
|
|
4945
|
+
async function dropSqliteShadow(shadowPath) {
|
|
4946
|
+
await unlink(shadowPath).catch(() => {
|
|
4947
|
+
});
|
|
4948
|
+
await unlink(shadowPath + "-wal").catch(() => {
|
|
4949
|
+
});
|
|
4950
|
+
await unlink(shadowPath + "-shm").catch(() => {
|
|
4951
|
+
});
|
|
4952
|
+
}
|
|
4953
|
+
function shadowName(config, id) {
|
|
4954
|
+
if (config.dialect === "sqlite") {
|
|
4955
|
+
return join(tmpdir(), `migraguard_shadow_${id}.sqlite3`);
|
|
4956
|
+
}
|
|
4957
|
+
return `migraguard_shadow_${id}`;
|
|
4958
|
+
}
|
|
4959
|
+
async function createShadow(config, name) {
|
|
4960
|
+
switch (config.dialect) {
|
|
4961
|
+
case "mysql":
|
|
4962
|
+
return createMysqlShadow(config, name);
|
|
4963
|
+
case "sqlite":
|
|
4964
|
+
return;
|
|
4965
|
+
// created on first access
|
|
4966
|
+
default:
|
|
4967
|
+
return createPgShadow(config.connection, name);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
async function dropShadow(config, name) {
|
|
4971
|
+
switch (config.dialect) {
|
|
4972
|
+
case "mysql":
|
|
4973
|
+
return dropMysqlShadow(config, name);
|
|
4974
|
+
case "sqlite":
|
|
4975
|
+
return dropSqliteShadow(name);
|
|
4976
|
+
default:
|
|
4977
|
+
return dropPgShadow(config.connection, name);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
async function cloneSourceToShadow(config, name) {
|
|
4981
|
+
switch (config.dialect) {
|
|
4982
|
+
case "mysql":
|
|
4983
|
+
return dumpMysqlSourceToShadow(config, name);
|
|
4984
|
+
case "sqlite":
|
|
4985
|
+
return dumpSqliteSourceToShadow(config, name);
|
|
4986
|
+
default:
|
|
4987
|
+
return dumpPgSourceToShadow(config, name);
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
4990
|
+
function buildShadowConfig(config, name) {
|
|
4541
4991
|
return {
|
|
4542
4992
|
...config,
|
|
4543
|
-
connection: { ...config.connection, database:
|
|
4544
|
-
dump: {
|
|
4545
|
-
...config.dump,
|
|
4546
|
-
pgDumpCommand: void 0
|
|
4547
|
-
}
|
|
4993
|
+
connection: { ...config.connection, database: name },
|
|
4994
|
+
dump: { ...config.dump, pgDumpCommand: void 0 }
|
|
4548
4995
|
};
|
|
4549
4996
|
}
|
|
4550
|
-
function
|
|
4997
|
+
function buildShadowDumpConfig(config, name) {
|
|
4998
|
+
if (config.dialect !== "postgresql") {
|
|
4999
|
+
return buildShadowConfig(config, name);
|
|
5000
|
+
}
|
|
4551
5001
|
const basePgDumpCmd = config.dump.pgDumpCommand;
|
|
4552
5002
|
let pgDumpCommand;
|
|
4553
5003
|
if (basePgDumpCmd && basePgDumpCmd.length > 0) {
|
|
4554
5004
|
pgDumpCommand = [...basePgDumpCmd];
|
|
4555
5005
|
const dbFlagIdx = pgDumpCommand.indexOf("-d");
|
|
4556
5006
|
if (dbFlagIdx >= 0 && dbFlagIdx + 1 < pgDumpCommand.length) {
|
|
4557
|
-
pgDumpCommand[dbFlagIdx + 1] =
|
|
5007
|
+
pgDumpCommand[dbFlagIdx + 1] = name;
|
|
4558
5008
|
} else {
|
|
4559
|
-
pgDumpCommand.push("-d",
|
|
5009
|
+
pgDumpCommand.push("-d", name);
|
|
4560
5010
|
}
|
|
4561
5011
|
}
|
|
4562
5012
|
return {
|
|
4563
5013
|
...config,
|
|
4564
|
-
connection: { ...config.connection, database:
|
|
5014
|
+
connection: { ...config.connection, database: name },
|
|
4565
5015
|
dump: { ...config.dump, pgDumpCommand }
|
|
4566
5016
|
};
|
|
4567
5017
|
}
|
|
4568
5018
|
async function getAppliedFiles(config) {
|
|
4569
|
-
const
|
|
4570
|
-
host: config.connection.host,
|
|
4571
|
-
port: config.connection.port,
|
|
4572
|
-
database: config.connection.database,
|
|
4573
|
-
user: config.connection.user,
|
|
4574
|
-
password: config.connection.password
|
|
4575
|
-
});
|
|
5019
|
+
const db = createDb(config);
|
|
4576
5020
|
try {
|
|
4577
|
-
await
|
|
4578
|
-
|
|
4579
|
-
|
|
5021
|
+
await db.connect();
|
|
5022
|
+
await db.ensureTable();
|
|
5023
|
+
const records = await db.getAllRecords();
|
|
5024
|
+
return new Set(
|
|
5025
|
+
records.filter((r) => r.status === "applied" || r.status === "skipped").map((r) => r.fileName)
|
|
4580
5026
|
);
|
|
4581
|
-
return new Set(result.rows.map((r) => r["file_name"]));
|
|
4582
5027
|
} catch {
|
|
4583
5028
|
return /* @__PURE__ */ new Set();
|
|
4584
5029
|
} finally {
|
|
4585
|
-
await
|
|
5030
|
+
await db.close();
|
|
4586
5031
|
}
|
|
4587
5032
|
}
|
|
4588
5033
|
async function verifyFile(file, sConfig, sDumpConfig) {
|
|
4589
|
-
const firstApply = await
|
|
5034
|
+
const firstApply = await executeSqlFile(sConfig, file.filePath);
|
|
4590
5035
|
if (!firstApply.success) {
|
|
4591
|
-
return {
|
|
4592
|
-
fileName: file.fileName,
|
|
4593
|
-
passed: false,
|
|
4594
|
-
firstApplyError: firstApply.stderr.trim()
|
|
4595
|
-
};
|
|
5036
|
+
return { fileName: file.fileName, passed: false, firstApplyError: firstApply.stderr.trim() };
|
|
4596
5037
|
}
|
|
4597
5038
|
const snapshot1 = await dumpSchema(sDumpConfig);
|
|
4598
|
-
const secondApply = await
|
|
5039
|
+
const secondApply = await executeSqlFile(sConfig, file.filePath);
|
|
4599
5040
|
if (!secondApply.success) {
|
|
4600
|
-
return {
|
|
4601
|
-
fileName: file.fileName,
|
|
4602
|
-
passed: false,
|
|
4603
|
-
secondApplyError: secondApply.stderr.trim()
|
|
4604
|
-
};
|
|
5041
|
+
return { fileName: file.fileName, passed: false, secondApplyError: secondApply.stderr.trim() };
|
|
4605
5042
|
}
|
|
4606
5043
|
const snapshot2 = await dumpSchema(sDumpConfig);
|
|
4607
5044
|
const drift = snapshot1 !== snapshot2;
|
|
4608
|
-
return {
|
|
4609
|
-
fileName: file.fileName,
|
|
4610
|
-
passed: !drift,
|
|
4611
|
-
schemaDrift: drift || void 0
|
|
4612
|
-
};
|
|
5045
|
+
return { fileName: file.fileName, passed: !drift, schemaDrift: drift || void 0 };
|
|
4613
5046
|
}
|
|
4614
5047
|
async function commandVerify(config, options) {
|
|
4615
5048
|
const allMode = options?.all ?? false;
|
|
4616
|
-
const
|
|
5049
|
+
const id = shadowId();
|
|
5050
|
+
const name = shadowName(config, id);
|
|
4617
5051
|
const files = await scanMigrations(config);
|
|
4618
5052
|
if (files.length === 0) {
|
|
4619
5053
|
console.log(chalk.yellow("No migration files to verify."));
|
|
4620
|
-
return { files: [], passed: 0, failed: 0, shadowDbName:
|
|
5054
|
+
return { files: [], passed: 0, failed: 0, shadowDbName: name };
|
|
4621
5055
|
}
|
|
4622
5056
|
const results = [];
|
|
4623
5057
|
console.log(chalk.bold(`
|
|
4624
|
-
Verifying idempotency using shadow DB: ${
|
|
5058
|
+
Verifying idempotency using shadow DB: ${name}`));
|
|
4625
5059
|
if (allMode) {
|
|
4626
5060
|
console.log(chalk.gray(" Mode: --all (verify all migrations from scratch)\n"));
|
|
4627
5061
|
} else {
|
|
4628
5062
|
console.log(chalk.gray(" Mode: incremental (restore current DB, verify pending)\n"));
|
|
4629
5063
|
}
|
|
4630
5064
|
try {
|
|
4631
|
-
await
|
|
4632
|
-
const sConfig =
|
|
4633
|
-
const sDumpConfig =
|
|
5065
|
+
await createShadow(config, name);
|
|
5066
|
+
const sConfig = buildShadowConfig(config, name);
|
|
5067
|
+
const sDumpConfig = buildShadowDumpConfig(config, name);
|
|
4634
5068
|
const verifiableFiles = files.filter((f) => f.phase !== "backfill");
|
|
4635
5069
|
if (allMode) {
|
|
4636
5070
|
for (const file of verifiableFiles) {
|
|
4637
5071
|
const r = await verifyFile(file, sConfig, sDumpConfig);
|
|
4638
5072
|
results.push(r);
|
|
4639
|
-
|
|
4640
|
-
console.log(chalk.green(` \u2713 ${file.fileName}`));
|
|
4641
|
-
} else {
|
|
4642
|
-
console.log(chalk.red(` \u2717 ${file.fileName}`));
|
|
4643
|
-
if (r.firstApplyError) console.log(chalk.red(` 1st apply error: ${r.firstApplyError}`));
|
|
4644
|
-
if (r.secondApplyError) console.log(chalk.red(` 2nd apply error: ${r.secondApplyError}`));
|
|
4645
|
-
if (r.schemaDrift) console.log(chalk.red(` Schema changed between 1st and 2nd apply`));
|
|
4646
|
-
}
|
|
5073
|
+
printVerifyFileResult(r);
|
|
4647
5074
|
}
|
|
4648
5075
|
} else {
|
|
4649
5076
|
console.log(chalk.blue(" Restoring current DB schema to shadow..."));
|
|
4650
|
-
await
|
|
5077
|
+
await cloneSourceToShadow(config, name);
|
|
4651
5078
|
console.log(chalk.green(" \u2713 Schema restored.\n"));
|
|
4652
5079
|
const appliedFiles = await getAppliedFiles(config);
|
|
4653
5080
|
const pendingFiles = verifiableFiles.filter((f) => !appliedFiles.has(f.fileName));
|
|
@@ -4659,21 +5086,14 @@ Verifying idempotency using shadow DB: ${dbName}`));
|
|
|
4659
5086
|
for (const file of pendingFiles) {
|
|
4660
5087
|
const r = await verifyFile(file, sConfig, sDumpConfig);
|
|
4661
5088
|
results.push(r);
|
|
4662
|
-
|
|
4663
|
-
console.log(chalk.green(` \u2713 ${file.fileName}`));
|
|
4664
|
-
} else {
|
|
4665
|
-
console.log(chalk.red(` \u2717 ${file.fileName}`));
|
|
4666
|
-
if (r.firstApplyError) console.log(chalk.red(` 1st apply error: ${r.firstApplyError}`));
|
|
4667
|
-
if (r.secondApplyError) console.log(chalk.red(` 2nd apply error: ${r.secondApplyError}`));
|
|
4668
|
-
if (r.schemaDrift) console.log(chalk.red(` Schema changed between 1st and 2nd apply`));
|
|
4669
|
-
}
|
|
5089
|
+
printVerifyFileResult(r);
|
|
4670
5090
|
}
|
|
4671
5091
|
}
|
|
4672
5092
|
}
|
|
4673
5093
|
} finally {
|
|
4674
|
-
await
|
|
5094
|
+
await dropShadow(config, name);
|
|
4675
5095
|
console.log(chalk.gray(`
|
|
4676
|
-
Shadow DB "${
|
|
5096
|
+
Shadow DB "${name}" dropped.`));
|
|
4677
5097
|
}
|
|
4678
5098
|
const passed = results.filter((r) => r.passed).length;
|
|
4679
5099
|
const failed = results.filter((r) => !r.passed).length;
|
|
@@ -4683,7 +5103,17 @@ Verifying idempotency using shadow DB: ${dbName}`));
|
|
|
4683
5103
|
} else if (failed > 0) {
|
|
4684
5104
|
console.log(chalk.red(`\u2717 ${failed}/${results.length} migration(s) failed idempotency check.`));
|
|
4685
5105
|
}
|
|
4686
|
-
return { files: results, passed, failed, shadowDbName:
|
|
5106
|
+
return { files: results, passed, failed, shadowDbName: name };
|
|
5107
|
+
}
|
|
5108
|
+
function printVerifyFileResult(r) {
|
|
5109
|
+
if (r.passed) {
|
|
5110
|
+
console.log(chalk.green(` \u2713 ${r.fileName}`));
|
|
5111
|
+
} else {
|
|
5112
|
+
console.log(chalk.red(` \u2717 ${r.fileName}`));
|
|
5113
|
+
if (r.firstApplyError) console.log(chalk.red(` 1st apply error: ${r.firstApplyError}`));
|
|
5114
|
+
if (r.secondApplyError) console.log(chalk.red(` 2nd apply error: ${r.secondApplyError}`));
|
|
5115
|
+
if (r.schemaDrift) console.log(chalk.red(` Schema changed between 1st and 2nd apply`));
|
|
5116
|
+
}
|
|
4687
5117
|
}
|
|
4688
5118
|
async function commandDeps(config, options = {}) {
|
|
4689
5119
|
const graph = await buildDependencyGraph(config);
|