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 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
- var { Client } = pg;
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(CREATE_TABLE_SQL);
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))`, [ADVISORY_LOCK_KEY]);
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))`, [ADVISORY_LOCK_KEY]);
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(mapRow);
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(mapRow);
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 mapRow(row) {
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
- var DEFAULT_CONNECTION = {
183
- host: "localhost",
184
- port: 5432,
185
- database: "postgres",
186
- user: "postgres"
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
- return {
256
- host: process.env["PGHOST"] ?? connection.host,
257
- port: process.env["PGPORT"] ? parseInt(process.env["PGPORT"], 10) : connection.port,
258
- database: process.env["PGDATABASE"] ?? connection.database,
259
- user: process.env["PGUSER"] ?? connection.user,
260
- password: process.env["PGPASSWORD"] ?? connection.password
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
- ...DEFAULT_CONNECTION,
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
- init_psql();
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 filtered) {
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 = new MigraguardDb(config);
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 executePsqlFile(config, filePath);
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 executePsqlFile(config, filePath);
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 executePsqlFile(config, filePath);
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 { executePsqlFile: execPsql } = await Promise.resolve().then(() => (init_psql(), psql_exports));
1633
- const psqlResult = await execPsql(config, schemaPath);
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 = new MigraguardDb(config);
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 = new MigraguardDb(config);
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 = new MigraguardDb(config);
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
- init_psql();
2188
+ init_executor();
1826
2189
  async function commandApplyPhase(config, options) {
1827
2190
  const { group, phase } = options;
1828
- const db = new MigraguardDb(config);
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 executePsqlFile(config, file.filePath);
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 = new MigraguardDb(config);
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 = new MigraguardDb(config);
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 = new MigraguardDb(config);
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 = new MigraguardDb(config);
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
- init_psql();
4830
+ init_executor();
4468
4831
  var { Client: Client2 } = pg;
4469
4832
  var execFileAsync3 = promisify(execFile);
4470
- function shadowDbName() {
4471
- const suffix = randomBytes(4).toString("hex");
4472
- return `migraguard_shadow_${suffix}`;
4833
+ function shadowId() {
4834
+ return randomBytes(4).toString("hex");
4473
4835
  }
4474
- function buildEnv(conn) {
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 createShadowDb(conn, dbName) {
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 dropShadowDb(conn, dbName) {
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 dumpSourceToShadow(config, shadowName) {
4874
+ async function dumpPgSourceToShadow(config, shadowName2) {
4513
4875
  const conn = config.connection;
4514
- const env = buildEnv(conn);
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 = buildEnv(conn);
4530
- restoreEnv["PGDATABASE"] = shadowName;
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 shadowConfig(config, shadowName) {
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: shadowName },
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 shadowDumpConfig(config, shadowName) {
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] = shadowName;
5007
+ pgDumpCommand[dbFlagIdx + 1] = name;
4558
5008
  } else {
4559
- pgDumpCommand.push("-d", shadowName);
5009
+ pgDumpCommand.push("-d", name);
4560
5010
  }
4561
5011
  }
4562
5012
  return {
4563
5013
  ...config,
4564
- connection: { ...config.connection, database: shadowName },
5014
+ connection: { ...config.connection, database: name },
4565
5015
  dump: { ...config.dump, pgDumpCommand }
4566
5016
  };
4567
5017
  }
4568
5018
  async function getAppliedFiles(config) {
4569
- const client = new Client2({
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 client.connect();
4578
- const result = await client.query(
4579
- `SELECT DISTINCT file_name FROM schema_migrations WHERE status IN ('applied', 'skipped')`
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 client.end();
5030
+ await db.close();
4586
5031
  }
4587
5032
  }
4588
5033
  async function verifyFile(file, sConfig, sDumpConfig) {
4589
- const firstApply = await executePsqlFile(sConfig, file.filePath);
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 executePsqlFile(sConfig, file.filePath);
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 dbName = shadowDbName();
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: dbName };
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: ${dbName}`));
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 createShadowDb(config.connection, dbName);
4632
- const sConfig = shadowConfig(config, dbName);
4633
- const sDumpConfig = shadowDumpConfig(config, dbName);
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
- if (r.passed) {
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 dumpSourceToShadow(config, dbName);
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
- if (r.passed) {
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 dropShadowDb(config.connection, dbName);
5094
+ await dropShadow(config, name);
4675
5095
  console.log(chalk.gray(`
4676
- Shadow DB "${dbName}" dropped.`));
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: dbName };
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);