@db-bridge/core 1.0.0 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3000 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'util';
3
+ import { resolve, basename, extname, join } from 'path';
4
+ import { existsSync } from 'fs';
5
+ import { pathToFileURL } from 'url';
6
+ import { createHash } from 'crypto';
7
+ import { mkdir, writeFile, readdir, readFile } from 'fs/promises';
8
+ import { hostname } from 'os';
9
+
10
+ var CONFIG_FILES = [
11
+ "db-bridge.config.ts",
12
+ "db-bridge.config.js",
13
+ "db-bridge.config.mjs",
14
+ "dbbridge.config.ts",
15
+ "dbbridge.config.js"
16
+ ];
17
+ async function loadConfig(cwd = process.cwd()) {
18
+ let configPath = null;
19
+ for (const filename of CONFIG_FILES) {
20
+ const fullPath = resolve(cwd, filename);
21
+ if (existsSync(fullPath)) {
22
+ configPath = fullPath;
23
+ break;
24
+ }
25
+ }
26
+ if (!configPath) {
27
+ throw new Error(`Configuration file not found. Create one of: ${CONFIG_FILES.join(", ")}`);
28
+ }
29
+ try {
30
+ const fileUrl = pathToFileURL(configPath).href;
31
+ const module = await import(fileUrl);
32
+ const config = module.default || module;
33
+ validateConfig(config);
34
+ return applyDefaults(config);
35
+ } catch (error2) {
36
+ if (error2.message.includes("Configuration file not found")) {
37
+ throw error2;
38
+ }
39
+ throw new Error(`Failed to load config from ${configPath}: ${error2.message}`);
40
+ }
41
+ }
42
+ function validateConfig(config) {
43
+ if (!config || typeof config !== "object") {
44
+ throw new Error("Configuration must be an object");
45
+ }
46
+ const cfg = config;
47
+ const connection = cfg["connection"];
48
+ if (!connection || typeof connection !== "object") {
49
+ throw new Error('Configuration must have a "connection" object');
50
+ }
51
+ const conn = connection;
52
+ const dialect = conn["dialect"];
53
+ const host = conn["host"];
54
+ const database = conn["database"];
55
+ if (!["mysql", "postgresql"].includes(dialect)) {
56
+ throw new Error('connection.dialect must be "mysql" or "postgresql"');
57
+ }
58
+ if (!host || typeof host !== "string") {
59
+ throw new Error("connection.host is required");
60
+ }
61
+ if (!database || typeof database !== "string") {
62
+ throw new Error("connection.database is required");
63
+ }
64
+ }
65
+ function applyDefaults(config) {
66
+ const defaultPort = config.connection.dialect === "mysql" ? 3306 : 5432;
67
+ return {
68
+ ...config,
69
+ connection: {
70
+ ...config.connection,
71
+ port: config.connection.port ?? defaultPort,
72
+ user: config.connection.user ?? "root",
73
+ password: config.connection.password ?? ""
74
+ },
75
+ migrations: {
76
+ directory: "./src/migrations",
77
+ tableName: "db_migrations",
78
+ lockTableName: "db_migrations_lock",
79
+ ...config.migrations
80
+ },
81
+ seeds: {
82
+ directory: "./src/seeds",
83
+ ...config.seeds
84
+ }
85
+ };
86
+ }
87
+ var MIGRATION_PATTERN = /^(\d{14})_(.+)\.(ts|js|mjs)$/;
88
+ var MigrationLoader = class {
89
+ directory;
90
+ extensions;
91
+ constructor(directory, extensions = [".ts", ".js", ".mjs"]) {
92
+ this.directory = directory;
93
+ this.extensions = extensions;
94
+ }
95
+ /**
96
+ * Scan directory for migration files
97
+ */
98
+ async scanDirectory() {
99
+ const files = [];
100
+ try {
101
+ const entries = await readdir(this.directory, { withFileTypes: true });
102
+ for (const entry of entries) {
103
+ if (!entry.isFile()) {
104
+ continue;
105
+ }
106
+ const ext = extname(entry.name);
107
+ if (!this.extensions.includes(ext)) {
108
+ continue;
109
+ }
110
+ const match = entry.name.match(MIGRATION_PATTERN);
111
+ if (!match) {
112
+ continue;
113
+ }
114
+ const [, timestamp, description] = match;
115
+ files.push({
116
+ name: basename(entry.name, ext),
117
+ path: join(this.directory, entry.name),
118
+ timestamp,
119
+ description: description.replaceAll("_", " ")
120
+ });
121
+ }
122
+ files.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
123
+ return files;
124
+ } catch (error2) {
125
+ if (error2.code === "ENOENT") {
126
+ throw new Error(`Migration directory not found: ${this.directory}`);
127
+ }
128
+ throw error2;
129
+ }
130
+ }
131
+ /**
132
+ * Load a single migration file
133
+ */
134
+ async loadMigration(file) {
135
+ try {
136
+ const fileUrl = pathToFileURL(file.path).href;
137
+ const module = await import(fileUrl);
138
+ const migration = module.default || module;
139
+ if (typeof migration.up !== "function") {
140
+ throw new TypeError(`Migration ${file.name} is missing 'up' function`);
141
+ }
142
+ if (typeof migration.down !== "function") {
143
+ throw new TypeError(`Migration ${file.name} is missing 'down' function`);
144
+ }
145
+ return {
146
+ name: file.name,
147
+ up: migration.up,
148
+ down: migration.down,
149
+ transactional: migration.transactional ?? true,
150
+ phase: migration.phase
151
+ };
152
+ } catch (error2) {
153
+ throw new Error(`Failed to load migration ${file.name}: ${error2.message}`);
154
+ }
155
+ }
156
+ /**
157
+ * Load all migrations from directory
158
+ */
159
+ async loadAll() {
160
+ const files = await this.scanDirectory();
161
+ const migrations = [];
162
+ for (const file of files) {
163
+ const migration = await this.loadMigration(file);
164
+ migrations.push(migration);
165
+ }
166
+ return migrations;
167
+ }
168
+ /**
169
+ * Get migration files (metadata only, without loading)
170
+ */
171
+ async getMigrationFiles() {
172
+ return this.scanDirectory();
173
+ }
174
+ /**
175
+ * Calculate checksum for a migration file
176
+ */
177
+ async calculateChecksum(file) {
178
+ const content = await readFile(file.path, "utf8");
179
+ const normalized = content.replaceAll("\r\n", "\n").trim();
180
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
181
+ }
182
+ /**
183
+ * Calculate checksums for all migration files
184
+ */
185
+ async calculateAllChecksums() {
186
+ const files = await this.scanDirectory();
187
+ const checksums = /* @__PURE__ */ new Map();
188
+ for (const file of files) {
189
+ const checksum = await this.calculateChecksum(file);
190
+ checksums.set(file.name, checksum);
191
+ }
192
+ return checksums;
193
+ }
194
+ /**
195
+ * Generate a new migration filename
196
+ */
197
+ static generateFilename(description) {
198
+ const now = /* @__PURE__ */ new Date();
199
+ const timestamp = [
200
+ now.getFullYear(),
201
+ String(now.getMonth() + 1).padStart(2, "0"),
202
+ String(now.getDate()).padStart(2, "0"),
203
+ String(now.getHours()).padStart(2, "0"),
204
+ String(now.getMinutes()).padStart(2, "0"),
205
+ String(now.getSeconds()).padStart(2, "0")
206
+ ].join("");
207
+ const sanitizedDescription = description.toLowerCase().replaceAll(/[^\da-z]+/g, "_").replaceAll(/^_+|_+$/g, "");
208
+ return `${timestamp}_${sanitizedDescription}.ts`;
209
+ }
210
+ /**
211
+ * Get migration template content
212
+ */
213
+ static getMigrationTemplate(name) {
214
+ return `/**
215
+ * Migration: ${name}
216
+ * Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
217
+ */
218
+
219
+ import type { SchemaBuilder } from '@db-bridge/core';
220
+
221
+ export default {
222
+ name: '${name}',
223
+
224
+ async up(schema: SchemaBuilder): Promise<void> {
225
+ // Write your migration here
226
+ // Example:
227
+ // await schema.createTable('users', (table) => {
228
+ // table.increments('id');
229
+ // table.string('email', 255).unique().notNull();
230
+ // table.timestamps();
231
+ // });
232
+ },
233
+
234
+ async down(schema: SchemaBuilder): Promise<void> {
235
+ // Reverse the migration
236
+ // Example:
237
+ // await schema.dropTableIfExists('users');
238
+ },
239
+ };
240
+ `;
241
+ }
242
+ };
243
+
244
+ // src/schema/dialects/MySQLDialect.ts
245
+ var MySQLDialect = class {
246
+ dialect = "mysql";
247
+ /**
248
+ * Quote an identifier (table/column name)
249
+ */
250
+ quoteIdentifier(name) {
251
+ return `\`${name.replaceAll("`", "``")}\``;
252
+ }
253
+ /**
254
+ * Quote a value for SQL
255
+ */
256
+ quoteValue(value) {
257
+ if (value === null || value === void 0) {
258
+ return "NULL";
259
+ }
260
+ if (typeof value === "boolean") {
261
+ return value ? "1" : "0";
262
+ }
263
+ if (typeof value === "number") {
264
+ return String(value);
265
+ }
266
+ if (typeof value === "string") {
267
+ return `'${value.replaceAll("'", "''")}'`;
268
+ }
269
+ return `'${String(value).replaceAll("'", "''")}'`;
270
+ }
271
+ /**
272
+ * Generate CREATE TABLE statement
273
+ */
274
+ createTable(definition) {
275
+ const parts = [];
276
+ for (const column of definition.columns) {
277
+ parts.push(this.columnToSQL(column));
278
+ }
279
+ if (definition.primaryKey && definition.primaryKey.length > 0) {
280
+ const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
281
+ parts.push(`PRIMARY KEY (${pkColumns})`);
282
+ }
283
+ for (const index of definition.indexes) {
284
+ parts.push(this.indexToSQL(index));
285
+ }
286
+ for (const fk of definition.foreignKeys) {
287
+ parts.push(this.foreignKeyToSQL(fk));
288
+ }
289
+ let sql = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
290
+ ${parts.join(",\n ")}
291
+ )`;
292
+ const options = [];
293
+ if (definition.engine) {
294
+ options.push(`ENGINE=${definition.engine}`);
295
+ } else {
296
+ options.push("ENGINE=InnoDB");
297
+ }
298
+ if (definition.charset) {
299
+ options.push(`DEFAULT CHARSET=${definition.charset}`);
300
+ } else {
301
+ options.push("DEFAULT CHARSET=utf8mb4");
302
+ }
303
+ if (definition.collation) {
304
+ options.push(`COLLATE=${definition.collation}`);
305
+ }
306
+ if (definition.comment) {
307
+ options.push(`COMMENT=${this.quoteValue(definition.comment)}`);
308
+ }
309
+ if (options.length > 0) {
310
+ sql += ` ${options.join(" ")}`;
311
+ }
312
+ return sql;
313
+ }
314
+ /**
315
+ * Generate DROP TABLE statement
316
+ */
317
+ dropTable(tableName) {
318
+ return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
319
+ }
320
+ /**
321
+ * Generate DROP TABLE IF EXISTS statement
322
+ */
323
+ dropTableIfExists(tableName) {
324
+ return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
325
+ }
326
+ /**
327
+ * Generate RENAME TABLE statement
328
+ */
329
+ renameTable(from, to) {
330
+ return `RENAME TABLE ${this.quoteIdentifier(from)} TO ${this.quoteIdentifier(to)}`;
331
+ }
332
+ /**
333
+ * Generate ALTER TABLE statements
334
+ */
335
+ alterTable(definition) {
336
+ const statements = [];
337
+ const tableName = this.quoteIdentifier(definition.tableName);
338
+ for (const op of definition.operations) {
339
+ switch (op.type) {
340
+ case "addColumn": {
341
+ statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
342
+ break;
343
+ }
344
+ case "dropColumn": {
345
+ statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
346
+ break;
347
+ }
348
+ case "renameColumn": {
349
+ statements.push(
350
+ `ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
351
+ );
352
+ break;
353
+ }
354
+ case "modifyColumn": {
355
+ statements.push(`ALTER TABLE ${tableName} MODIFY COLUMN ${this.columnToSQL(op.column)}`);
356
+ break;
357
+ }
358
+ case "addIndex": {
359
+ statements.push(`ALTER TABLE ${tableName} ADD ${this.indexToSQL(op.index)}`);
360
+ break;
361
+ }
362
+ case "dropIndex": {
363
+ statements.push(`ALTER TABLE ${tableName} DROP INDEX ${this.quoteIdentifier(op.name)}`);
364
+ break;
365
+ }
366
+ case "addForeignKey": {
367
+ statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
368
+ break;
369
+ }
370
+ case "dropForeignKey": {
371
+ statements.push(
372
+ `ALTER TABLE ${tableName} DROP FOREIGN KEY ${this.quoteIdentifier(op.name)}`
373
+ );
374
+ break;
375
+ }
376
+ case "addPrimary": {
377
+ const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
378
+ statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
379
+ break;
380
+ }
381
+ case "dropPrimary": {
382
+ statements.push(`ALTER TABLE ${tableName} DROP PRIMARY KEY`);
383
+ break;
384
+ }
385
+ }
386
+ }
387
+ return statements;
388
+ }
389
+ /**
390
+ * Generate query to check if table exists
391
+ */
392
+ hasTable(tableName) {
393
+ return `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
394
+ }
395
+ /**
396
+ * Generate query to check if column exists
397
+ */
398
+ hasColumn(tableName, columnName) {
399
+ return `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ${this.quoteValue(tableName)} AND column_name = ${this.quoteValue(columnName)} LIMIT 1`;
400
+ }
401
+ /**
402
+ * Convert column definition to SQL
403
+ */
404
+ columnToSQL(column) {
405
+ const parts = [this.quoteIdentifier(column.name)];
406
+ parts.push(this.columnTypeToSQL(column));
407
+ if (column.unsigned) {
408
+ parts.push("UNSIGNED");
409
+ }
410
+ if (column.nullable) {
411
+ parts.push("NULL");
412
+ } else {
413
+ parts.push("NOT NULL");
414
+ }
415
+ if (column.defaultRaw) {
416
+ parts.push(`DEFAULT ${column.defaultRaw}`);
417
+ } else if (column.defaultValue !== void 0) {
418
+ parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
419
+ }
420
+ if (column.autoIncrement) {
421
+ parts.push("AUTO_INCREMENT");
422
+ }
423
+ if (column.primary && !column.autoIncrement) {
424
+ parts.push("PRIMARY KEY");
425
+ } else if (column.primary && column.autoIncrement) {
426
+ parts.push("PRIMARY KEY");
427
+ }
428
+ if (column.unique && !column.primary) {
429
+ parts.push("UNIQUE");
430
+ }
431
+ if (column.comment) {
432
+ parts.push(`COMMENT ${this.quoteValue(column.comment)}`);
433
+ }
434
+ if (column.first) {
435
+ parts.push("FIRST");
436
+ } else if (column.after) {
437
+ parts.push(`AFTER ${this.quoteIdentifier(column.after)}`);
438
+ }
439
+ return parts.join(" ");
440
+ }
441
+ /**
442
+ * Convert column type to MySQL type
443
+ */
444
+ columnTypeToSQL(column) {
445
+ switch (column.type) {
446
+ case "increments": {
447
+ return "INT UNSIGNED AUTO_INCREMENT";
448
+ }
449
+ case "bigIncrements": {
450
+ return "BIGINT UNSIGNED AUTO_INCREMENT";
451
+ }
452
+ case "integer": {
453
+ return "INT";
454
+ }
455
+ case "bigInteger": {
456
+ return "BIGINT";
457
+ }
458
+ case "smallInteger": {
459
+ return "SMALLINT";
460
+ }
461
+ case "tinyInteger": {
462
+ return "TINYINT";
463
+ }
464
+ case "float": {
465
+ return "FLOAT";
466
+ }
467
+ case "double": {
468
+ return "DOUBLE";
469
+ }
470
+ case "decimal": {
471
+ const precision = column.precision ?? 10;
472
+ const scale = column.scale ?? 2;
473
+ return `DECIMAL(${precision},${scale})`;
474
+ }
475
+ case "string": {
476
+ return `VARCHAR(${column.length ?? 255})`;
477
+ }
478
+ case "text": {
479
+ return "TEXT";
480
+ }
481
+ case "mediumText": {
482
+ return "MEDIUMTEXT";
483
+ }
484
+ case "longText": {
485
+ return "LONGTEXT";
486
+ }
487
+ case "boolean": {
488
+ return "TINYINT(1)";
489
+ }
490
+ case "date": {
491
+ return "DATE";
492
+ }
493
+ case "datetime": {
494
+ return "DATETIME";
495
+ }
496
+ case "timestamp": {
497
+ return "TIMESTAMP";
498
+ }
499
+ case "time": {
500
+ return "TIME";
501
+ }
502
+ case "json":
503
+ case "jsonb": {
504
+ return "JSON";
505
+ }
506
+ case "uuid": {
507
+ return "CHAR(36)";
508
+ }
509
+ case "binary": {
510
+ return "BLOB";
511
+ }
512
+ case "enum": {
513
+ if (column.enumValues && column.enumValues.length > 0) {
514
+ const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
515
+ return `ENUM(${values})`;
516
+ }
517
+ return "VARCHAR(255)";
518
+ }
519
+ default: {
520
+ return "VARCHAR(255)";
521
+ }
522
+ }
523
+ }
524
+ /**
525
+ * Convert index definition to SQL
526
+ */
527
+ indexToSQL(index) {
528
+ const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
529
+ const name = index.name ? this.quoteIdentifier(index.name) : "";
530
+ if (index.type === "fulltext") {
531
+ return `FULLTEXT INDEX ${name} (${columns})`;
532
+ }
533
+ if (index.unique) {
534
+ return `UNIQUE INDEX ${name} (${columns})`;
535
+ }
536
+ return `INDEX ${name} (${columns})`;
537
+ }
538
+ /**
539
+ * Convert foreign key definition to SQL
540
+ */
541
+ foreignKeyToSQL(fk) {
542
+ const parts = ["CONSTRAINT"];
543
+ if (fk.name) {
544
+ parts.push(this.quoteIdentifier(fk.name));
545
+ }
546
+ parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
547
+ parts.push(
548
+ `REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
549
+ );
550
+ if (fk.onDelete) {
551
+ parts.push(`ON DELETE ${fk.onDelete}`);
552
+ }
553
+ if (fk.onUpdate) {
554
+ parts.push(`ON UPDATE ${fk.onUpdate}`);
555
+ }
556
+ return parts.join(" ");
557
+ }
558
+ };
559
+
560
+ // src/schema/dialects/PostgreSQLDialect.ts
561
+ var PostgreSQLDialect = class {
562
+ dialect = "postgresql";
563
+ /**
564
+ * Quote an identifier (table/column name)
565
+ */
566
+ quoteIdentifier(name) {
567
+ return `"${name.replaceAll('"', '""')}"`;
568
+ }
569
+ /**
570
+ * Quote a value for SQL
571
+ */
572
+ quoteValue(value) {
573
+ if (value === null || value === void 0) {
574
+ return "NULL";
575
+ }
576
+ if (typeof value === "boolean") {
577
+ return value ? "TRUE" : "FALSE";
578
+ }
579
+ if (typeof value === "number") {
580
+ return String(value);
581
+ }
582
+ if (typeof value === "string") {
583
+ return `'${value.replaceAll("'", "''")}'`;
584
+ }
585
+ return `'${String(value).replaceAll("'", "''")}'`;
586
+ }
587
+ /**
588
+ * Generate CREATE TABLE statement
589
+ */
590
+ createTable(definition) {
591
+ const parts = [];
592
+ for (const column of definition.columns) {
593
+ parts.push(this.columnToSQL(column));
594
+ }
595
+ if (definition.primaryKey && definition.primaryKey.length > 0) {
596
+ const pkColumns = definition.primaryKey.map((c) => this.quoteIdentifier(c)).join(", ");
597
+ parts.push(`PRIMARY KEY (${pkColumns})`);
598
+ }
599
+ for (const fk of definition.foreignKeys) {
600
+ parts.push(this.foreignKeyToSQL(fk));
601
+ }
602
+ const sql = `CREATE TABLE ${this.quoteIdentifier(definition.name)} (
603
+ ${parts.join(",\n ")}
604
+ )`;
605
+ const statements = [sql];
606
+ if (definition.comment) {
607
+ statements.push(
608
+ `COMMENT ON TABLE ${this.quoteIdentifier(definition.name)} IS ${this.quoteValue(definition.comment)}`
609
+ );
610
+ }
611
+ return statements.join(";\n");
612
+ }
613
+ /**
614
+ * Generate DROP TABLE statement
615
+ */
616
+ dropTable(tableName) {
617
+ return `DROP TABLE ${this.quoteIdentifier(tableName)}`;
618
+ }
619
+ /**
620
+ * Generate DROP TABLE IF EXISTS statement
621
+ */
622
+ dropTableIfExists(tableName) {
623
+ return `DROP TABLE IF EXISTS ${this.quoteIdentifier(tableName)}`;
624
+ }
625
+ /**
626
+ * Generate ALTER TABLE ... RENAME statement
627
+ */
628
+ renameTable(from, to) {
629
+ return `ALTER TABLE ${this.quoteIdentifier(from)} RENAME TO ${this.quoteIdentifier(to)}`;
630
+ }
631
+ /**
632
+ * Generate ALTER TABLE statements
633
+ */
634
+ alterTable(definition) {
635
+ const statements = [];
636
+ const tableName = this.quoteIdentifier(definition.tableName);
637
+ for (const op of definition.operations) {
638
+ switch (op.type) {
639
+ case "addColumn": {
640
+ statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${this.columnToSQL(op.column)}`);
641
+ break;
642
+ }
643
+ case "dropColumn": {
644
+ statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.quoteIdentifier(op.name)}`);
645
+ break;
646
+ }
647
+ case "renameColumn": {
648
+ statements.push(
649
+ `ALTER TABLE ${tableName} RENAME COLUMN ${this.quoteIdentifier(op.from)} TO ${this.quoteIdentifier(op.to)}`
650
+ );
651
+ break;
652
+ }
653
+ case "modifyColumn": {
654
+ const col = op.column;
655
+ statements.push(
656
+ `ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} TYPE ${this.columnTypeToSQL(col)}`
657
+ );
658
+ if (col.nullable) {
659
+ statements.push(
660
+ `ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} DROP NOT NULL`
661
+ );
662
+ } else {
663
+ statements.push(
664
+ `ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET NOT NULL`
665
+ );
666
+ }
667
+ if (col.defaultRaw) {
668
+ statements.push(
669
+ `ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${col.defaultRaw}`
670
+ );
671
+ } else if (col.defaultValue !== void 0) {
672
+ statements.push(
673
+ `ALTER TABLE ${tableName} ALTER COLUMN ${this.quoteIdentifier(col.name)} SET DEFAULT ${this.quoteValue(col.defaultValue)}`
674
+ );
675
+ }
676
+ break;
677
+ }
678
+ case "addIndex": {
679
+ statements.push(this.createIndex(definition.tableName, op.index));
680
+ break;
681
+ }
682
+ case "dropIndex": {
683
+ statements.push(`DROP INDEX ${this.quoteIdentifier(op.name)}`);
684
+ break;
685
+ }
686
+ case "addForeignKey": {
687
+ statements.push(`ALTER TABLE ${tableName} ADD ${this.foreignKeyToSQL(op.foreignKey)}`);
688
+ break;
689
+ }
690
+ case "dropForeignKey": {
691
+ statements.push(
692
+ `ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(op.name)}`
693
+ );
694
+ break;
695
+ }
696
+ case "addPrimary": {
697
+ const pkCols = op.columns.map((c) => this.quoteIdentifier(c)).join(", ");
698
+ statements.push(`ALTER TABLE ${tableName} ADD PRIMARY KEY (${pkCols})`);
699
+ break;
700
+ }
701
+ case "dropPrimary": {
702
+ statements.push(
703
+ `ALTER TABLE ${tableName} DROP CONSTRAINT ${this.quoteIdentifier(`${definition.tableName}_pkey`)}`
704
+ );
705
+ break;
706
+ }
707
+ }
708
+ }
709
+ return statements;
710
+ }
711
+ /**
712
+ * Generate query to check if table exists
713
+ */
714
+ hasTable(tableName) {
715
+ return `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${this.quoteValue(tableName)} LIMIT 1`;
716
+ }
717
+ /**
718
+ * Generate query to check if column exists
719
+ */
720
+ hasColumn(tableName, columnName) {
721
+ return `SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ${this.quoteValue(tableName)} AND column_name = ${this.quoteValue(columnName)} LIMIT 1`;
722
+ }
723
+ /**
724
+ * Create index statement
725
+ */
726
+ createIndex(tableName, index) {
727
+ const columns = index.columns.map((c) => this.quoteIdentifier(c)).join(", ");
728
+ const indexName = index.name || `idx_${tableName}_${index.columns.join("_")}`;
729
+ const unique = index.unique ? "UNIQUE " : "";
730
+ return `CREATE ${unique}INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteIdentifier(tableName)} (${columns})`;
731
+ }
732
+ /**
733
+ * Convert column definition to SQL
734
+ */
735
+ columnToSQL(column) {
736
+ const parts = [this.quoteIdentifier(column.name)];
737
+ parts.push(this.columnTypeToSQL(column));
738
+ if (!column.nullable) {
739
+ parts.push("NOT NULL");
740
+ }
741
+ if (column.defaultRaw) {
742
+ parts.push(`DEFAULT ${column.defaultRaw}`);
743
+ } else if (column.defaultValue !== void 0) {
744
+ parts.push(`DEFAULT ${this.quoteValue(column.defaultValue)}`);
745
+ }
746
+ if (column.primary && column.type !== "increments" && column.type !== "bigIncrements") {
747
+ parts.push("PRIMARY KEY");
748
+ }
749
+ if (column.unique && !column.primary) {
750
+ parts.push("UNIQUE");
751
+ }
752
+ return parts.join(" ");
753
+ }
754
+ /**
755
+ * Convert column type to PostgreSQL type
756
+ */
757
+ columnTypeToSQL(column) {
758
+ switch (column.type) {
759
+ case "increments": {
760
+ return "SERIAL PRIMARY KEY";
761
+ }
762
+ case "bigIncrements": {
763
+ return "BIGSERIAL PRIMARY KEY";
764
+ }
765
+ case "integer": {
766
+ return "INTEGER";
767
+ }
768
+ case "bigInteger": {
769
+ return "BIGINT";
770
+ }
771
+ case "smallInteger": {
772
+ return "SMALLINT";
773
+ }
774
+ case "tinyInteger": {
775
+ return "SMALLINT";
776
+ }
777
+ // PostgreSQL doesn't have TINYINT
778
+ case "float": {
779
+ return "REAL";
780
+ }
781
+ case "double": {
782
+ return "DOUBLE PRECISION";
783
+ }
784
+ case "decimal": {
785
+ const precision = column.precision ?? 10;
786
+ const scale = column.scale ?? 2;
787
+ return `NUMERIC(${precision},${scale})`;
788
+ }
789
+ case "string": {
790
+ return `VARCHAR(${column.length ?? 255})`;
791
+ }
792
+ case "text":
793
+ case "mediumText":
794
+ case "longText": {
795
+ return "TEXT";
796
+ }
797
+ case "boolean": {
798
+ return "BOOLEAN";
799
+ }
800
+ case "date": {
801
+ return "DATE";
802
+ }
803
+ case "datetime":
804
+ case "timestamp": {
805
+ return "TIMESTAMP";
806
+ }
807
+ case "time": {
808
+ return "TIME";
809
+ }
810
+ case "json": {
811
+ return "JSON";
812
+ }
813
+ case "jsonb": {
814
+ return "JSONB";
815
+ }
816
+ case "uuid": {
817
+ return "UUID";
818
+ }
819
+ case "binary": {
820
+ return "BYTEA";
821
+ }
822
+ case "enum": {
823
+ if (column.enumValues && column.enumValues.length > 0) {
824
+ const values = column.enumValues.map((v) => this.quoteValue(v)).join(", ");
825
+ return `VARCHAR(255) CHECK (${this.quoteIdentifier(column.name)} IN (${values}))`;
826
+ }
827
+ return "VARCHAR(255)";
828
+ }
829
+ default: {
830
+ return "VARCHAR(255)";
831
+ }
832
+ }
833
+ }
834
+ /**
835
+ * Convert foreign key definition to SQL
836
+ */
837
+ foreignKeyToSQL(fk) {
838
+ const parts = ["CONSTRAINT"];
839
+ if (fk.name) {
840
+ parts.push(this.quoteIdentifier(fk.name));
841
+ } else {
842
+ parts.push(this.quoteIdentifier(`fk_${fk.column}_${fk.table}`));
843
+ }
844
+ parts.push(`FOREIGN KEY (${this.quoteIdentifier(fk.column)})`);
845
+ parts.push(
846
+ `REFERENCES ${this.quoteIdentifier(fk.table)} (${this.quoteIdentifier(fk.referenceColumn)})`
847
+ );
848
+ if (fk.onDelete) {
849
+ parts.push(`ON DELETE ${fk.onDelete}`);
850
+ }
851
+ if (fk.onUpdate) {
852
+ parts.push(`ON UPDATE ${fk.onUpdate}`);
853
+ }
854
+ return parts.join(" ");
855
+ }
856
+ };
857
+
858
+ // src/migrations/MigrationLock.ts
859
+ var MigrationLock = class {
860
+ adapter;
861
+ tableName;
862
+ timeout;
863
+ dialect;
864
+ lockId;
865
+ isLocked = false;
866
+ constructor(adapter, options) {
867
+ this.adapter = adapter;
868
+ this.tableName = options.tableName ?? "db_migrations_lock";
869
+ this.timeout = options.timeout ?? 6e4;
870
+ this.lockId = `${hostname()}_${process.pid}_${Date.now()}`;
871
+ this.dialect = options.dialect === "mysql" ? new MySQLDialect() : new PostgreSQLDialect();
872
+ }
873
+ /**
874
+ * Initialize the lock table
875
+ */
876
+ async initialize() {
877
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
878
+ if (this.dialect.dialect === "mysql") {
879
+ await this.adapter.execute(`
880
+ CREATE TABLE IF NOT EXISTS ${tableName} (
881
+ id INT PRIMARY KEY,
882
+ is_locked TINYINT NOT NULL DEFAULT 0,
883
+ locked_at TIMESTAMP NULL,
884
+ locked_by VARCHAR(255) NULL
885
+ ) ENGINE=InnoDB
886
+ `);
887
+ } else {
888
+ await this.adapter.execute(`
889
+ CREATE TABLE IF NOT EXISTS ${tableName} (
890
+ id INT PRIMARY KEY,
891
+ is_locked BOOLEAN NOT NULL DEFAULT FALSE,
892
+ locked_at TIMESTAMP NULL,
893
+ locked_by VARCHAR(255) NULL
894
+ )
895
+ `);
896
+ }
897
+ const checkSql = this.dialect.dialect === "mysql" ? `SELECT 1 FROM ${tableName} WHERE id = 1` : `SELECT 1 FROM ${tableName} WHERE id = 1`;
898
+ const result = await this.adapter.query(checkSql);
899
+ if (result.rows.length === 0) {
900
+ if (this.dialect.dialect === "mysql") {
901
+ await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, 0)`);
902
+ } else {
903
+ await this.adapter.execute(`INSERT INTO ${tableName} (id, is_locked) VALUES (1, FALSE)`);
904
+ }
905
+ }
906
+ }
907
+ /**
908
+ * Acquire the migration lock
909
+ */
910
+ async acquire() {
911
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
912
+ const startTime = Date.now();
913
+ while (Date.now() - startTime < this.timeout) {
914
+ try {
915
+ if (this.dialect.dialect === "mysql") {
916
+ const result = await this.adapter.execute(
917
+ `
918
+ UPDATE ${tableName}
919
+ SET is_locked = 1,
920
+ locked_at = NOW(),
921
+ locked_by = ?
922
+ WHERE id = 1 AND is_locked = 0
923
+ `,
924
+ [this.lockId]
925
+ );
926
+ if ((result.affectedRows ?? 0) > 0) {
927
+ this.isLocked = true;
928
+ return true;
929
+ }
930
+ } else {
931
+ const result = await this.adapter.execute(
932
+ `
933
+ UPDATE ${tableName}
934
+ SET is_locked = TRUE,
935
+ locked_at = NOW(),
936
+ locked_by = $1
937
+ WHERE id = 1 AND is_locked = FALSE
938
+ RETURNING id
939
+ `,
940
+ [this.lockId]
941
+ );
942
+ if (result.rows.length > 0) {
943
+ this.isLocked = true;
944
+ return true;
945
+ }
946
+ }
947
+ const staleResult = await this.adapter.query(`SELECT locked_at, locked_by FROM ${tableName} WHERE id = 1`);
948
+ if (staleResult.rows.length > 0) {
949
+ const lockedAt = staleResult.rows[0].locked_at;
950
+ if (lockedAt && Date.now() - new Date(lockedAt).getTime() > this.timeout) {
951
+ console.warn(
952
+ `Releasing stale migration lock held by ${staleResult.rows[0].locked_by}`
953
+ );
954
+ await this.forceRelease();
955
+ continue;
956
+ }
957
+ }
958
+ await this.sleep(1e3);
959
+ } catch (error2) {
960
+ console.error("Error acquiring migration lock:", error2);
961
+ throw error2;
962
+ }
963
+ }
964
+ return false;
965
+ }
966
+ /**
967
+ * Release the migration lock
968
+ */
969
+ async release() {
970
+ if (!this.isLocked) {
971
+ return;
972
+ }
973
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
974
+ try {
975
+ if (this.dialect.dialect === "mysql") {
976
+ await this.adapter.execute(
977
+ `
978
+ UPDATE ${tableName}
979
+ SET is_locked = 0,
980
+ locked_at = NULL,
981
+ locked_by = NULL
982
+ WHERE id = 1 AND locked_by = ?
983
+ `,
984
+ [this.lockId]
985
+ );
986
+ } else {
987
+ await this.adapter.execute(
988
+ `
989
+ UPDATE ${tableName}
990
+ SET is_locked = FALSE,
991
+ locked_at = NULL,
992
+ locked_by = NULL
993
+ WHERE id = 1 AND locked_by = $1
994
+ `,
995
+ [this.lockId]
996
+ );
997
+ }
998
+ this.isLocked = false;
999
+ } catch (error2) {
1000
+ console.error("Error releasing migration lock:", error2);
1001
+ throw error2;
1002
+ }
1003
+ }
1004
+ /**
1005
+ * Force release the lock (for stale locks)
1006
+ */
1007
+ async forceRelease() {
1008
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
1009
+ if (this.dialect.dialect === "mysql") {
1010
+ await this.adapter.execute(`
1011
+ UPDATE ${tableName}
1012
+ SET is_locked = 0,
1013
+ locked_at = NULL,
1014
+ locked_by = NULL
1015
+ WHERE id = 1
1016
+ `);
1017
+ } else {
1018
+ await this.adapter.execute(`
1019
+ UPDATE ${tableName}
1020
+ SET is_locked = FALSE,
1021
+ locked_at = NULL,
1022
+ locked_by = NULL
1023
+ WHERE id = 1
1024
+ `);
1025
+ }
1026
+ this.isLocked = false;
1027
+ }
1028
+ /**
1029
+ * Check if lock is currently held
1030
+ */
1031
+ async isHeld() {
1032
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
1033
+ const result = await this.adapter.query(
1034
+ `SELECT is_locked FROM ${tableName} WHERE id = 1`
1035
+ );
1036
+ if (result.rows.length === 0) {
1037
+ return false;
1038
+ }
1039
+ const isLocked = result.rows[0].is_locked;
1040
+ return isLocked === 1 || isLocked === true;
1041
+ }
1042
+ /**
1043
+ * Get lock info
1044
+ */
1045
+ async getLockInfo() {
1046
+ const tableName = this.dialect.quoteIdentifier(this.tableName);
1047
+ const result = await this.adapter.query(`SELECT is_locked, locked_at, locked_by FROM ${tableName} WHERE id = 1`);
1048
+ if (result.rows.length === 0) {
1049
+ return { isLocked: false, lockedAt: null, lockedBy: null };
1050
+ }
1051
+ const row = result.rows[0];
1052
+ return {
1053
+ isLocked: row.is_locked === 1 || row.is_locked === true,
1054
+ lockedAt: row.locked_at,
1055
+ lockedBy: row.locked_by
1056
+ };
1057
+ }
1058
+ /**
1059
+ * Execute a function with lock
1060
+ */
1061
+ async withLock(fn) {
1062
+ const acquired = await this.acquire();
1063
+ if (!acquired) {
1064
+ throw new Error(
1065
+ `Could not acquire migration lock within ${this.timeout}ms. Another migration may be running.`
1066
+ );
1067
+ }
1068
+ try {
1069
+ return await fn();
1070
+ } finally {
1071
+ await this.release();
1072
+ }
1073
+ }
1074
+ sleep(ms) {
1075
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
1076
+ }
1077
+ };
1078
+
1079
+ // src/schema/ColumnBuilder.ts
1080
+ var ColumnBuilder = class {
1081
+ definition;
1082
+ constructor(name, type) {
1083
+ this.definition = {
1084
+ name,
1085
+ type,
1086
+ nullable: type !== "increments" && type !== "bigIncrements",
1087
+ unsigned: false,
1088
+ autoIncrement: type === "increments" || type === "bigIncrements",
1089
+ primary: type === "increments" || type === "bigIncrements",
1090
+ unique: false,
1091
+ index: false,
1092
+ first: false
1093
+ };
1094
+ }
1095
+ /**
1096
+ * Set column length (for string types)
1097
+ */
1098
+ length(length) {
1099
+ this.definition.length = length;
1100
+ return this;
1101
+ }
1102
+ /**
1103
+ * Set precision and scale (for decimal types)
1104
+ */
1105
+ precision(precision, scale) {
1106
+ this.definition.precision = precision;
1107
+ this.definition.scale = scale;
1108
+ return this;
1109
+ }
1110
+ /**
1111
+ * Mark column as nullable
1112
+ */
1113
+ nullable() {
1114
+ this.definition.nullable = true;
1115
+ return this;
1116
+ }
1117
+ /**
1118
+ * Mark column as not nullable
1119
+ */
1120
+ notNull() {
1121
+ this.definition.nullable = false;
1122
+ return this;
1123
+ }
1124
+ /**
1125
+ * Alias for notNull()
1126
+ */
1127
+ notNullable() {
1128
+ return this.notNull();
1129
+ }
1130
+ /**
1131
+ * Set default value
1132
+ */
1133
+ default(value) {
1134
+ this.definition.defaultValue = value;
1135
+ return this;
1136
+ }
1137
+ /**
1138
+ * Set default value as raw SQL
1139
+ */
1140
+ defaultRaw(sql) {
1141
+ this.definition.defaultRaw = sql;
1142
+ return this;
1143
+ }
1144
+ /**
1145
+ * Set default to current timestamp
1146
+ */
1147
+ defaultNow() {
1148
+ this.definition.defaultRaw = "CURRENT_TIMESTAMP";
1149
+ return this;
1150
+ }
1151
+ /**
1152
+ * Mark column as unsigned (MySQL)
1153
+ */
1154
+ unsigned() {
1155
+ this.definition.unsigned = true;
1156
+ return this;
1157
+ }
1158
+ /**
1159
+ * Mark column as auto increment
1160
+ */
1161
+ autoIncrement() {
1162
+ this.definition.autoIncrement = true;
1163
+ return this;
1164
+ }
1165
+ /**
1166
+ * Mark column as primary key
1167
+ */
1168
+ primary() {
1169
+ this.definition.primary = true;
1170
+ return this;
1171
+ }
1172
+ /**
1173
+ * Mark column as unique
1174
+ */
1175
+ unique() {
1176
+ this.definition.unique = true;
1177
+ return this;
1178
+ }
1179
+ /**
1180
+ * Add index to column
1181
+ */
1182
+ index() {
1183
+ this.definition.index = true;
1184
+ return this;
1185
+ }
1186
+ /**
1187
+ * Add comment to column
1188
+ */
1189
+ comment(comment) {
1190
+ this.definition.comment = comment;
1191
+ return this;
1192
+ }
1193
+ /**
1194
+ * Position column after another column (MySQL)
1195
+ */
1196
+ after(columnName) {
1197
+ this.definition.after = columnName;
1198
+ return this;
1199
+ }
1200
+ /**
1201
+ * Position column first (MySQL)
1202
+ */
1203
+ first() {
1204
+ this.definition.first = true;
1205
+ return this;
1206
+ }
1207
+ /**
1208
+ * Set enum values
1209
+ */
1210
+ values(values) {
1211
+ this.definition.enumValues = values;
1212
+ return this;
1213
+ }
1214
+ /**
1215
+ * Add foreign key reference
1216
+ */
1217
+ references(column) {
1218
+ const fkBuilder = new ForeignKeyBuilder(this, column);
1219
+ return fkBuilder;
1220
+ }
1221
+ /**
1222
+ * Set foreign key definition (internal)
1223
+ */
1224
+ setForeignKey(fk) {
1225
+ this.definition.references = fk;
1226
+ }
1227
+ /**
1228
+ * Get the column definition
1229
+ */
1230
+ getDefinition() {
1231
+ return { ...this.definition };
1232
+ }
1233
+ };
1234
+ var ForeignKeyBuilder = class {
1235
+ columnBuilder;
1236
+ fkDefinition;
1237
+ constructor(columnBuilder, referenceColumn) {
1238
+ this.columnBuilder = columnBuilder;
1239
+ this.fkDefinition = {
1240
+ column: columnBuilder.getDefinition().name,
1241
+ referenceColumn
1242
+ };
1243
+ }
1244
+ /**
1245
+ * Set the referenced table
1246
+ */
1247
+ on(tableName) {
1248
+ this.fkDefinition.table = tableName;
1249
+ this.applyToColumn();
1250
+ return this;
1251
+ }
1252
+ /**
1253
+ * Alias for on()
1254
+ */
1255
+ inTable(tableName) {
1256
+ return this.on(tableName);
1257
+ }
1258
+ /**
1259
+ * Set ON DELETE action
1260
+ */
1261
+ onDelete(action) {
1262
+ this.fkDefinition.onDelete = action;
1263
+ this.applyToColumn();
1264
+ return this;
1265
+ }
1266
+ /**
1267
+ * Set ON UPDATE action
1268
+ */
1269
+ onUpdate(action) {
1270
+ this.fkDefinition.onUpdate = action;
1271
+ this.applyToColumn();
1272
+ return this;
1273
+ }
1274
+ /**
1275
+ * Set constraint name
1276
+ */
1277
+ name(name) {
1278
+ this.fkDefinition.name = name;
1279
+ this.applyToColumn();
1280
+ return this;
1281
+ }
1282
+ /**
1283
+ * Apply the foreign key definition to the column
1284
+ */
1285
+ applyToColumn() {
1286
+ if (this.fkDefinition.table && this.fkDefinition.referenceColumn) {
1287
+ this.columnBuilder.setForeignKey(this.fkDefinition);
1288
+ }
1289
+ }
1290
+ /**
1291
+ * Get the column builder for chaining
1292
+ */
1293
+ getColumnBuilder() {
1294
+ return this.columnBuilder;
1295
+ }
1296
+ };
1297
+
1298
+ // src/schema/TableBuilder.ts
1299
+ var TableBuilder = class {
1300
+ tableName;
1301
+ columns = [];
1302
+ indexes = [];
1303
+ foreignKeys = [];
1304
+ primaryKeyColumns;
1305
+ tableEngine;
1306
+ tableCharset;
1307
+ tableCollation;
1308
+ tableComment;
1309
+ constructor(tableName) {
1310
+ this.tableName = tableName;
1311
+ }
1312
+ // ============================================
1313
+ // Column Types
1314
+ // ============================================
1315
+ /**
1316
+ * Auto-incrementing integer primary key
1317
+ */
1318
+ increments(name = "id") {
1319
+ return this.addColumn(name, "increments");
1320
+ }
1321
+ /**
1322
+ * Auto-incrementing big integer primary key
1323
+ */
1324
+ bigIncrements(name = "id") {
1325
+ return this.addColumn(name, "bigIncrements");
1326
+ }
1327
+ /**
1328
+ * Integer column
1329
+ */
1330
+ integer(name) {
1331
+ return this.addColumn(name, "integer");
1332
+ }
1333
+ /**
1334
+ * Big integer column
1335
+ */
1336
+ bigInteger(name) {
1337
+ return this.addColumn(name, "bigInteger");
1338
+ }
1339
+ /**
1340
+ * Small integer column
1341
+ */
1342
+ smallInteger(name) {
1343
+ return this.addColumn(name, "smallInteger");
1344
+ }
1345
+ /**
1346
+ * Tiny integer column
1347
+ */
1348
+ tinyInteger(name) {
1349
+ return this.addColumn(name, "tinyInteger");
1350
+ }
1351
+ /**
1352
+ * Float column
1353
+ */
1354
+ float(name) {
1355
+ return this.addColumn(name, "float");
1356
+ }
1357
+ /**
1358
+ * Double column
1359
+ */
1360
+ double(name) {
1361
+ return this.addColumn(name, "double");
1362
+ }
1363
+ /**
1364
+ * Decimal column
1365
+ */
1366
+ decimal(name, precision = 10, scale = 2) {
1367
+ return this.addColumn(name, "decimal").precision(precision, scale);
1368
+ }
1369
+ /**
1370
+ * String (VARCHAR) column
1371
+ */
1372
+ string(name, length = 255) {
1373
+ return this.addColumn(name, "string").length(length);
1374
+ }
1375
+ /**
1376
+ * Text column
1377
+ */
1378
+ text(name) {
1379
+ return this.addColumn(name, "text");
1380
+ }
1381
+ /**
1382
+ * Medium text column
1383
+ */
1384
+ mediumText(name) {
1385
+ return this.addColumn(name, "mediumText");
1386
+ }
1387
+ /**
1388
+ * Long text column
1389
+ */
1390
+ longText(name) {
1391
+ return this.addColumn(name, "longText");
1392
+ }
1393
+ /**
1394
+ * Boolean column
1395
+ */
1396
+ boolean(name) {
1397
+ return this.addColumn(name, "boolean");
1398
+ }
1399
+ /**
1400
+ * Date column
1401
+ */
1402
+ date(name) {
1403
+ return this.addColumn(name, "date");
1404
+ }
1405
+ /**
1406
+ * Datetime column
1407
+ */
1408
+ datetime(name) {
1409
+ return this.addColumn(name, "datetime");
1410
+ }
1411
+ /**
1412
+ * Timestamp column
1413
+ */
1414
+ timestamp(name) {
1415
+ return this.addColumn(name, "timestamp");
1416
+ }
1417
+ /**
1418
+ * Time column
1419
+ */
1420
+ time(name) {
1421
+ return this.addColumn(name, "time");
1422
+ }
1423
+ /**
1424
+ * JSON column
1425
+ */
1426
+ json(name) {
1427
+ return this.addColumn(name, "json");
1428
+ }
1429
+ /**
1430
+ * JSONB column (PostgreSQL)
1431
+ */
1432
+ jsonb(name) {
1433
+ return this.addColumn(name, "jsonb");
1434
+ }
1435
+ /**
1436
+ * UUID column
1437
+ */
1438
+ uuid(name) {
1439
+ return this.addColumn(name, "uuid");
1440
+ }
1441
+ /**
1442
+ * Binary column
1443
+ */
1444
+ binary(name) {
1445
+ return this.addColumn(name, "binary");
1446
+ }
1447
+ /**
1448
+ * Enum column
1449
+ */
1450
+ enum(name, values) {
1451
+ return this.addColumn(name, "enum").values(values);
1452
+ }
1453
+ // ============================================
1454
+ // Shortcut Methods
1455
+ // ============================================
1456
+ /**
1457
+ * Add created_at and updated_at timestamp columns
1458
+ */
1459
+ timestamps() {
1460
+ this.timestamp("created_at").notNull().defaultNow();
1461
+ this.timestamp("updated_at").notNull().defaultNow();
1462
+ }
1463
+ /**
1464
+ * Add deleted_at timestamp column for soft deletes
1465
+ */
1466
+ softDeletes() {
1467
+ return this.timestamp("deleted_at").nullable();
1468
+ }
1469
+ /**
1470
+ * Add foreign id column with foreign key
1471
+ */
1472
+ foreignId(name) {
1473
+ const tableName = `${name.replace(/_id$/, "")}s`;
1474
+ const column = this.integer(name).unsigned().notNull();
1475
+ this.foreign(name).references("id").on(tableName);
1476
+ return column;
1477
+ }
1478
+ /**
1479
+ * Add UUID primary key column
1480
+ */
1481
+ uuidPrimary(name = "id") {
1482
+ return this.uuid(name).primary().notNull();
1483
+ }
1484
+ // ============================================
1485
+ // Indexes
1486
+ // ============================================
1487
+ /**
1488
+ * Add an index
1489
+ */
1490
+ index(columns, name) {
1491
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1492
+ this.indexes.push({
1493
+ name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
1494
+ columns: columnArray,
1495
+ unique: false
1496
+ });
1497
+ return this;
1498
+ }
1499
+ /**
1500
+ * Add a unique index
1501
+ */
1502
+ unique(columns, name) {
1503
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1504
+ this.indexes.push({
1505
+ name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
1506
+ columns: columnArray,
1507
+ unique: true
1508
+ });
1509
+ return this;
1510
+ }
1511
+ /**
1512
+ * Add a fulltext index (MySQL)
1513
+ */
1514
+ fulltext(columns, name) {
1515
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1516
+ this.indexes.push({
1517
+ name: name || `ft_${this.tableName}_${columnArray.join("_")}`,
1518
+ columns: columnArray,
1519
+ unique: false,
1520
+ type: "fulltext"
1521
+ });
1522
+ return this;
1523
+ }
1524
+ /**
1525
+ * Set primary key columns
1526
+ */
1527
+ primary(columns) {
1528
+ this.primaryKeyColumns = Array.isArray(columns) ? columns : [columns];
1529
+ return this;
1530
+ }
1531
+ // ============================================
1532
+ // Foreign Keys
1533
+ // ============================================
1534
+ /**
1535
+ * Add a foreign key constraint
1536
+ */
1537
+ foreign(column) {
1538
+ return new ForeignKeyChain(this, column);
1539
+ }
1540
+ /**
1541
+ * Add foreign key (internal)
1542
+ */
1543
+ addForeignKey(fk) {
1544
+ this.foreignKeys.push(fk);
1545
+ }
1546
+ // ============================================
1547
+ // Table Options
1548
+ // ============================================
1549
+ /**
1550
+ * Set table engine (MySQL)
1551
+ */
1552
+ engine(engine) {
1553
+ this.tableEngine = engine;
1554
+ return this;
1555
+ }
1556
+ /**
1557
+ * Set table charset (MySQL)
1558
+ */
1559
+ charset(charset) {
1560
+ this.tableCharset = charset;
1561
+ return this;
1562
+ }
1563
+ /**
1564
+ * Set table collation (MySQL)
1565
+ */
1566
+ collation(collation) {
1567
+ this.tableCollation = collation;
1568
+ return this;
1569
+ }
1570
+ /**
1571
+ * Set table comment
1572
+ */
1573
+ comment(comment) {
1574
+ this.tableComment = comment;
1575
+ return this;
1576
+ }
1577
+ // ============================================
1578
+ // Internal Methods
1579
+ // ============================================
1580
+ /**
1581
+ * Add a column and return its builder
1582
+ */
1583
+ addColumn(name, type) {
1584
+ const builder = new ColumnBuilder(name, type);
1585
+ this.columns.push(builder.getDefinition());
1586
+ const index = this.columns.length - 1;
1587
+ const proxy = new Proxy(builder, {
1588
+ get: (target, prop, receiver) => {
1589
+ const result = Reflect.get(target, prop, receiver);
1590
+ if (typeof result === "function") {
1591
+ return (...args) => {
1592
+ const returnValue = result.apply(target, args);
1593
+ this.columns[index] = target.getDefinition();
1594
+ return returnValue === target ? proxy : returnValue;
1595
+ };
1596
+ }
1597
+ return result;
1598
+ }
1599
+ });
1600
+ return proxy;
1601
+ }
1602
+ /**
1603
+ * Get the table definition
1604
+ */
1605
+ getDefinition() {
1606
+ return {
1607
+ name: this.tableName,
1608
+ columns: [...this.columns],
1609
+ indexes: [...this.indexes],
1610
+ foreignKeys: [...this.foreignKeys],
1611
+ primaryKey: this.primaryKeyColumns,
1612
+ engine: this.tableEngine,
1613
+ charset: this.tableCharset,
1614
+ collation: this.tableCollation,
1615
+ comment: this.tableComment
1616
+ };
1617
+ }
1618
+ };
1619
+ var ForeignKeyChain = class {
1620
+ tableBuilder;
1621
+ fkDefinition;
1622
+ constructor(tableBuilder, column) {
1623
+ this.tableBuilder = tableBuilder;
1624
+ this.fkDefinition = { column };
1625
+ }
1626
+ /**
1627
+ * Set the referenced column
1628
+ */
1629
+ references(column) {
1630
+ this.fkDefinition.referenceColumn = column;
1631
+ return this;
1632
+ }
1633
+ /**
1634
+ * Set the referenced table
1635
+ */
1636
+ on(tableName) {
1637
+ this.fkDefinition.table = tableName;
1638
+ this.apply();
1639
+ return this;
1640
+ }
1641
+ /**
1642
+ * Alias for on()
1643
+ */
1644
+ inTable(tableName) {
1645
+ return this.on(tableName);
1646
+ }
1647
+ /**
1648
+ * Set ON DELETE action
1649
+ */
1650
+ onDelete(action) {
1651
+ this.fkDefinition.onDelete = action;
1652
+ this.apply();
1653
+ return this;
1654
+ }
1655
+ /**
1656
+ * Set ON UPDATE action
1657
+ */
1658
+ onUpdate(action) {
1659
+ this.fkDefinition.onUpdate = action;
1660
+ this.apply();
1661
+ return this;
1662
+ }
1663
+ /**
1664
+ * Set constraint name
1665
+ */
1666
+ name(name) {
1667
+ this.fkDefinition.name = name;
1668
+ this.apply();
1669
+ return this;
1670
+ }
1671
+ /**
1672
+ * Apply the foreign key to the table
1673
+ */
1674
+ apply() {
1675
+ if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
1676
+ if (!this.fkDefinition.name) {
1677
+ this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
1678
+ }
1679
+ this.tableBuilder.addForeignKey(this.fkDefinition);
1680
+ }
1681
+ }
1682
+ };
1683
+
1684
+ // src/schema/SchemaBuilder.ts
1685
+ var SchemaBuilder = class {
1686
+ dialectInstance;
1687
+ adapter;
1688
+ constructor(options) {
1689
+ this.adapter = options.adapter;
1690
+ switch (options.dialect) {
1691
+ case "mysql": {
1692
+ this.dialectInstance = new MySQLDialect();
1693
+ break;
1694
+ }
1695
+ case "postgresql": {
1696
+ this.dialectInstance = new PostgreSQLDialect();
1697
+ break;
1698
+ }
1699
+ default: {
1700
+ throw new Error(`Unsupported dialect: ${options.dialect}`);
1701
+ }
1702
+ }
1703
+ }
1704
+ /**
1705
+ * Get the dialect instance
1706
+ */
1707
+ get dialect() {
1708
+ return this.dialectInstance;
1709
+ }
1710
+ /**
1711
+ * Create a new table
1712
+ */
1713
+ async createTable(tableName, callback) {
1714
+ const builder = new TableBuilder(tableName);
1715
+ callback(builder);
1716
+ const definition = builder.getDefinition();
1717
+ const sql = this.dialectInstance.createTable(definition);
1718
+ await this.execute(sql);
1719
+ if (this.dialectInstance.dialect === "postgresql" && definition.indexes.length > 0) {
1720
+ const pgDialect = this.dialectInstance;
1721
+ for (const index of definition.indexes) {
1722
+ const indexSql = pgDialect.createIndex(tableName, index);
1723
+ await this.execute(indexSql);
1724
+ }
1725
+ }
1726
+ }
1727
+ /**
1728
+ * Create a table if it doesn't exist
1729
+ */
1730
+ async createTableIfNotExists(tableName, callback) {
1731
+ const exists = await this.hasTable(tableName);
1732
+ if (!exists) {
1733
+ await this.createTable(tableName, callback);
1734
+ }
1735
+ }
1736
+ /**
1737
+ * Drop a table
1738
+ */
1739
+ async dropTable(tableName) {
1740
+ const sql = this.dialectInstance.dropTable(tableName);
1741
+ await this.execute(sql);
1742
+ }
1743
+ /**
1744
+ * Drop a table if it exists
1745
+ */
1746
+ async dropTableIfExists(tableName) {
1747
+ const sql = this.dialectInstance.dropTableIfExists(tableName);
1748
+ await this.execute(sql);
1749
+ }
1750
+ /**
1751
+ * Rename a table
1752
+ */
1753
+ async renameTable(from, to) {
1754
+ const sql = this.dialectInstance.renameTable(from, to);
1755
+ await this.execute(sql);
1756
+ }
1757
+ /**
1758
+ * Check if a table exists
1759
+ */
1760
+ async hasTable(tableName) {
1761
+ const sql = this.dialectInstance.hasTable(tableName);
1762
+ const result = await this.query(sql);
1763
+ return result.length > 0;
1764
+ }
1765
+ /**
1766
+ * Check if a column exists in a table
1767
+ */
1768
+ async hasColumn(tableName, columnName) {
1769
+ const sql = this.dialectInstance.hasColumn(tableName, columnName);
1770
+ const result = await this.query(sql);
1771
+ return result.length > 0;
1772
+ }
1773
+ /**
1774
+ * Alter a table
1775
+ */
1776
+ async alterTable(tableName, callback) {
1777
+ const builder = new AlterTableBuilder(tableName);
1778
+ callback(builder);
1779
+ const definition = builder.getDefinition();
1780
+ const statements = this.dialectInstance.alterTable(definition);
1781
+ for (const sql of statements) {
1782
+ await this.execute(sql);
1783
+ }
1784
+ }
1785
+ /**
1786
+ * Execute raw SQL
1787
+ */
1788
+ async raw(sql, params) {
1789
+ await this.execute(sql, params);
1790
+ }
1791
+ /**
1792
+ * Execute SQL statement
1793
+ */
1794
+ async execute(sql, params) {
1795
+ if (this.adapter) {
1796
+ await this.adapter.execute(sql, params);
1797
+ } else {
1798
+ console.log("SQL:", sql);
1799
+ if (params && params.length > 0) {
1800
+ console.log("Params:", params);
1801
+ }
1802
+ }
1803
+ }
1804
+ /**
1805
+ * Execute SQL query
1806
+ */
1807
+ async query(sql) {
1808
+ if (this.adapter) {
1809
+ const result = await this.adapter.query(sql);
1810
+ return result.rows;
1811
+ }
1812
+ return [];
1813
+ }
1814
+ /**
1815
+ * Generate SQL without executing (for preview/dry-run)
1816
+ */
1817
+ generateCreateTableSQL(tableName, callback) {
1818
+ const builder = new TableBuilder(tableName);
1819
+ callback(builder);
1820
+ return this.dialectInstance.createTable(builder.getDefinition());
1821
+ }
1822
+ /**
1823
+ * Generate ALTER TABLE SQL without executing
1824
+ */
1825
+ generateAlterTableSQL(tableName, callback) {
1826
+ const builder = new AlterTableBuilder(tableName);
1827
+ callback(builder);
1828
+ return this.dialectInstance.alterTable(builder.getDefinition());
1829
+ }
1830
+ };
1831
+ var AlterTableBuilder = class {
1832
+ tableName;
1833
+ operations = [];
1834
+ constructor(tableName) {
1835
+ this.tableName = tableName;
1836
+ }
1837
+ /**
1838
+ * Add a new column
1839
+ */
1840
+ addColumn(name, type, options) {
1841
+ this.operations.push({
1842
+ type: "addColumn",
1843
+ column: {
1844
+ name,
1845
+ type,
1846
+ nullable: true,
1847
+ unsigned: false,
1848
+ autoIncrement: false,
1849
+ primary: false,
1850
+ unique: false,
1851
+ index: false,
1852
+ first: false,
1853
+ ...options
1854
+ }
1855
+ });
1856
+ return this;
1857
+ }
1858
+ /**
1859
+ * Drop a column
1860
+ */
1861
+ dropColumn(name) {
1862
+ this.operations.push({ type: "dropColumn", name });
1863
+ return this;
1864
+ }
1865
+ /**
1866
+ * Rename a column
1867
+ */
1868
+ renameColumn(from, to) {
1869
+ this.operations.push({ type: "renameColumn", from, to });
1870
+ return this;
1871
+ }
1872
+ /**
1873
+ * Modify a column
1874
+ */
1875
+ modifyColumn(name, type, options) {
1876
+ this.operations.push({
1877
+ type: "modifyColumn",
1878
+ column: {
1879
+ name,
1880
+ type,
1881
+ nullable: true,
1882
+ unsigned: false,
1883
+ autoIncrement: false,
1884
+ primary: false,
1885
+ unique: false,
1886
+ index: false,
1887
+ first: false,
1888
+ ...options
1889
+ }
1890
+ });
1891
+ return this;
1892
+ }
1893
+ /**
1894
+ * Add an index
1895
+ */
1896
+ addIndex(columns, name) {
1897
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1898
+ this.operations.push({
1899
+ type: "addIndex",
1900
+ index: {
1901
+ name: name || `idx_${this.tableName}_${columnArray.join("_")}`,
1902
+ columns: columnArray,
1903
+ unique: false
1904
+ }
1905
+ });
1906
+ return this;
1907
+ }
1908
+ /**
1909
+ * Add a unique index
1910
+ */
1911
+ addUnique(columns, name) {
1912
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1913
+ this.operations.push({
1914
+ type: "addIndex",
1915
+ index: {
1916
+ name: name || `uniq_${this.tableName}_${columnArray.join("_")}`,
1917
+ columns: columnArray,
1918
+ unique: true
1919
+ }
1920
+ });
1921
+ return this;
1922
+ }
1923
+ /**
1924
+ * Drop an index
1925
+ */
1926
+ dropIndex(name) {
1927
+ this.operations.push({ type: "dropIndex", name });
1928
+ return this;
1929
+ }
1930
+ /**
1931
+ * Add a foreign key
1932
+ */
1933
+ addForeign(column) {
1934
+ return new AlterForeignKeyBuilder(this, column);
1935
+ }
1936
+ /**
1937
+ * Add foreign key (internal)
1938
+ */
1939
+ addForeignKeyOperation(fk) {
1940
+ this.operations.push({ type: "addForeignKey", foreignKey: fk });
1941
+ }
1942
+ /**
1943
+ * Drop a foreign key
1944
+ */
1945
+ dropForeign(name) {
1946
+ this.operations.push({ type: "dropForeignKey", name });
1947
+ return this;
1948
+ }
1949
+ /**
1950
+ * Add primary key
1951
+ */
1952
+ addPrimary(columns) {
1953
+ const columnArray = Array.isArray(columns) ? columns : [columns];
1954
+ this.operations.push({ type: "addPrimary", columns: columnArray });
1955
+ return this;
1956
+ }
1957
+ /**
1958
+ * Drop primary key
1959
+ */
1960
+ dropPrimary() {
1961
+ this.operations.push({ type: "dropPrimary" });
1962
+ return this;
1963
+ }
1964
+ /**
1965
+ * Get the alter table definition
1966
+ */
1967
+ getDefinition() {
1968
+ return {
1969
+ tableName: this.tableName,
1970
+ operations: [...this.operations]
1971
+ };
1972
+ }
1973
+ };
1974
+ var AlterForeignKeyBuilder = class {
1975
+ builder;
1976
+ fkDefinition;
1977
+ constructor(builder, column) {
1978
+ this.builder = builder;
1979
+ this.fkDefinition = { column };
1980
+ }
1981
+ references(column) {
1982
+ this.fkDefinition.referenceColumn = column;
1983
+ return this;
1984
+ }
1985
+ on(tableName) {
1986
+ this.fkDefinition.table = tableName;
1987
+ this.apply();
1988
+ return this;
1989
+ }
1990
+ onDelete(action) {
1991
+ this.fkDefinition.onDelete = action;
1992
+ this.apply();
1993
+ return this;
1994
+ }
1995
+ onUpdate(action) {
1996
+ this.fkDefinition.onUpdate = action;
1997
+ this.apply();
1998
+ return this;
1999
+ }
2000
+ name(name) {
2001
+ this.fkDefinition.name = name;
2002
+ this.apply();
2003
+ return this;
2004
+ }
2005
+ apply() {
2006
+ if (this.fkDefinition.column && this.fkDefinition.table && this.fkDefinition.referenceColumn) {
2007
+ if (!this.fkDefinition.name) {
2008
+ this.fkDefinition.name = `fk_${this.fkDefinition.column}_${this.fkDefinition.table}`;
2009
+ }
2010
+ this.builder.addForeignKeyOperation(this.fkDefinition);
2011
+ }
2012
+ }
2013
+ };
2014
+
2015
+ // src/migrations/FileMigrationRunner.ts
2016
+ var FileMigrationRunner = class {
2017
+ adapter;
2018
+ loader;
2019
+ lock;
2020
+ schema;
2021
+ options;
2022
+ constructor(adapter, options) {
2023
+ this.adapter = adapter;
2024
+ this.options = {
2025
+ directory: options.directory,
2026
+ tableName: options.tableName ?? "db_migrations",
2027
+ lockTableName: options.lockTableName ?? "db_migrations_lock",
2028
+ lockTimeout: options.lockTimeout ?? 6e4,
2029
+ validateChecksums: options.validateChecksums ?? true,
2030
+ dialect: options.dialect,
2031
+ logger: options.logger ?? console,
2032
+ dryRun: options.dryRun ?? false
2033
+ };
2034
+ this.loader = new MigrationLoader(options.directory);
2035
+ this.lock = new MigrationLock(adapter, {
2036
+ tableName: this.options.lockTableName,
2037
+ timeout: this.options.lockTimeout,
2038
+ dialect: options.dialect
2039
+ });
2040
+ this.schema = new SchemaBuilder({
2041
+ dialect: options.dialect,
2042
+ adapter
2043
+ });
2044
+ }
2045
+ /**
2046
+ * Initialize migration tables
2047
+ */
2048
+ async initialize() {
2049
+ if (this.options.dryRun) {
2050
+ this.options.logger.info("DRY RUN: Would create migration tables");
2051
+ return;
2052
+ }
2053
+ const tableName = this.options.tableName;
2054
+ const hasTable = await this.schema.hasTable(tableName);
2055
+ if (!hasTable) {
2056
+ await this.schema.createTable(tableName, (table) => {
2057
+ table.increments("id");
2058
+ table.string("name", 255).notNull().unique();
2059
+ table.integer("batch").notNull();
2060
+ table.timestamp("executed_at").notNull().defaultNow();
2061
+ table.integer("execution_time_ms").notNull();
2062
+ table.string("checksum", 64).notNull();
2063
+ table.index("batch");
2064
+ table.index("executed_at");
2065
+ });
2066
+ this.options.logger.info(`Created migrations table: ${tableName}`);
2067
+ }
2068
+ await this.lock.initialize();
2069
+ }
2070
+ /**
2071
+ * Run all pending migrations
2072
+ */
2073
+ async latest() {
2074
+ await this.lock.initialize();
2075
+ return this.lock.withLock(async () => {
2076
+ await this.initialize();
2077
+ const pending = await this.getPendingMigrations();
2078
+ if (pending.length === 0) {
2079
+ this.options.logger.info("No pending migrations");
2080
+ return [];
2081
+ }
2082
+ const batch = await this.getNextBatch();
2083
+ const executed = [];
2084
+ this.options.logger.info(`Running ${pending.length} migrations (batch ${batch})`);
2085
+ for (const migration of pending) {
2086
+ await this.runMigration(migration, "up", batch);
2087
+ executed.push(migration.name);
2088
+ }
2089
+ return executed;
2090
+ });
2091
+ }
2092
+ /**
2093
+ * Rollback the last batch of migrations
2094
+ */
2095
+ async rollback(steps = 1) {
2096
+ await this.lock.initialize();
2097
+ return this.lock.withLock(async () => {
2098
+ await this.initialize();
2099
+ const batches = await this.getLastBatches(steps);
2100
+ if (batches.length === 0) {
2101
+ this.options.logger.info("No migrations to rollback");
2102
+ return [];
2103
+ }
2104
+ const rolledBack = [];
2105
+ for (const batch of batches) {
2106
+ this.options.logger.info(`Rolling back batch ${batch.batch}`);
2107
+ const migrations = await this.loadMigrationsByName(
2108
+ batch.migrations.map((m) => m.name).reverse()
2109
+ );
2110
+ for (const migration of migrations) {
2111
+ await this.runMigration(migration, "down", batch.batch);
2112
+ rolledBack.push(migration.name);
2113
+ }
2114
+ }
2115
+ return rolledBack;
2116
+ });
2117
+ }
2118
+ /**
2119
+ * Reset all migrations
2120
+ */
2121
+ async reset() {
2122
+ await this.lock.initialize();
2123
+ return this.lock.withLock(async () => {
2124
+ await this.initialize();
2125
+ const executed = await this.getExecutedMigrations();
2126
+ if (executed.length === 0) {
2127
+ this.options.logger.info("No migrations to reset");
2128
+ return [];
2129
+ }
2130
+ const rolledBack = [];
2131
+ const migrations = await this.loadMigrationsByName(executed.map((m) => m.name).reverse());
2132
+ for (const migration of migrations) {
2133
+ const record = executed.find((e) => e.name === migration.name);
2134
+ await this.runMigration(migration, "down", record.batch);
2135
+ rolledBack.push(migration.name);
2136
+ }
2137
+ return rolledBack;
2138
+ });
2139
+ }
2140
+ /**
2141
+ * Reset and re-run all migrations
2142
+ */
2143
+ async fresh() {
2144
+ await this.reset();
2145
+ return this.latest();
2146
+ }
2147
+ /**
2148
+ * Get migration status
2149
+ */
2150
+ async status() {
2151
+ await this.initialize();
2152
+ const allMigrations = await this.loader.getMigrationFiles();
2153
+ const executed = await this.getExecutedMigrations();
2154
+ const executedMap = new Map(executed.map((e) => [e.name, e]));
2155
+ return allMigrations.map((file) => {
2156
+ const record = executedMap.get(file.name);
2157
+ return {
2158
+ name: file.name,
2159
+ batch: record?.batch ?? null,
2160
+ executedAt: record?.executed_at ?? null,
2161
+ pending: !record
2162
+ };
2163
+ });
2164
+ }
2165
+ /**
2166
+ * Validate migrations
2167
+ */
2168
+ async validate() {
2169
+ const errors = [];
2170
+ try {
2171
+ const migrations = await this.loader.loadAll();
2172
+ const executed = await this.getExecutedMigrations();
2173
+ for (const record of executed) {
2174
+ const migration = migrations.find((m) => m.name === record.name);
2175
+ if (!migration) {
2176
+ errors.push(`Migration '${record.name}' exists in database but not in codebase`);
2177
+ continue;
2178
+ }
2179
+ if (this.options.validateChecksums) {
2180
+ const checksum = this.calculateChecksum(migration);
2181
+ if (checksum !== record.checksum) {
2182
+ errors.push(
2183
+ `Checksum mismatch for migration '${record.name}'. The migration may have been modified.`
2184
+ );
2185
+ }
2186
+ }
2187
+ }
2188
+ } catch (error2) {
2189
+ errors.push(`Validation error: ${error2.message}`);
2190
+ }
2191
+ return {
2192
+ valid: errors.length === 0,
2193
+ errors
2194
+ };
2195
+ }
2196
+ /**
2197
+ * Get pending migrations
2198
+ */
2199
+ async getPendingMigrations() {
2200
+ const allMigrations = await this.loader.loadAll();
2201
+ const executed = await this.getExecutedMigrations();
2202
+ const executedNames = new Set(executed.map((e) => e.name));
2203
+ return allMigrations.filter((m) => !executedNames.has(m.name));
2204
+ }
2205
+ /**
2206
+ * Get executed migrations from database
2207
+ */
2208
+ async getExecutedMigrations() {
2209
+ const result = await this.adapter.query(
2210
+ `SELECT * FROM ${this.options.tableName} ORDER BY id ASC`
2211
+ );
2212
+ return result.rows;
2213
+ }
2214
+ /**
2215
+ * Get the next batch number
2216
+ */
2217
+ async getNextBatch() {
2218
+ const result = await this.adapter.query(
2219
+ `SELECT MAX(batch) as max_batch FROM ${this.options.tableName}`
2220
+ );
2221
+ return (result.rows[0]?.max_batch ?? 0) + 1;
2222
+ }
2223
+ /**
2224
+ * Get the last N batches
2225
+ */
2226
+ async getLastBatches(count) {
2227
+ const result = await this.adapter.query(
2228
+ `SELECT * FROM ${this.options.tableName} ORDER BY batch DESC, id DESC`
2229
+ );
2230
+ const batches = /* @__PURE__ */ new Map();
2231
+ for (const record of result.rows) {
2232
+ if (!batches.has(record.batch)) {
2233
+ batches.set(record.batch, []);
2234
+ }
2235
+ batches.get(record.batch).push(record);
2236
+ }
2237
+ const batchNumbers = Array.from(batches.keys()).slice(0, count);
2238
+ return batchNumbers.map((batch) => ({
2239
+ batch,
2240
+ migrations: batches.get(batch)
2241
+ }));
2242
+ }
2243
+ /**
2244
+ * Load migrations by name
2245
+ */
2246
+ async loadMigrationsByName(names) {
2247
+ const allMigrations = await this.loader.loadAll();
2248
+ const migrationMap = new Map(allMigrations.map((m) => [m.name, m]));
2249
+ return names.map((name) => migrationMap.get(name)).filter((m) => m !== void 0);
2250
+ }
2251
+ /**
2252
+ * Run a single migration
2253
+ */
2254
+ async runMigration(migration, direction, batch) {
2255
+ const startTime = Date.now();
2256
+ const action = direction === "up" ? "Running" : "Rolling back";
2257
+ this.options.logger.info(`${action}: ${migration.name}`);
2258
+ if (this.options.dryRun) {
2259
+ this.options.logger.info(`DRY RUN: Would ${direction} ${migration.name}`);
2260
+ return;
2261
+ }
2262
+ const transaction = migration.transactional ? await this.adapter.beginTransaction() : null;
2263
+ try {
2264
+ const schema = new SchemaBuilder({
2265
+ dialect: this.options.dialect,
2266
+ adapter: this.adapter
2267
+ });
2268
+ await migration[direction](schema);
2269
+ const executionTime = Date.now() - startTime;
2270
+ if (direction === "up") {
2271
+ const checksum = this.calculateChecksum(migration);
2272
+ await this.adapter.execute(
2273
+ `INSERT INTO ${this.options.tableName}
2274
+ (name, batch, executed_at, execution_time_ms, checksum)
2275
+ VALUES (?, ?, NOW(), ?, ?)`,
2276
+ [migration.name, batch, executionTime, checksum]
2277
+ );
2278
+ } else {
2279
+ await this.adapter.execute(`DELETE FROM ${this.options.tableName} WHERE name = ?`, [
2280
+ migration.name
2281
+ ]);
2282
+ }
2283
+ if (transaction) {
2284
+ await transaction.commit();
2285
+ }
2286
+ this.options.logger.info(
2287
+ `${direction === "up" ? "Completed" : "Rolled back"}: ${migration.name} (${executionTime}ms)`
2288
+ );
2289
+ } catch (error2) {
2290
+ if (transaction) {
2291
+ await transaction.rollback();
2292
+ }
2293
+ throw new Error(
2294
+ `Failed to ${direction} migration '${migration.name}': ${error2.message}`
2295
+ );
2296
+ }
2297
+ }
2298
+ /**
2299
+ * Calculate migration checksum
2300
+ */
2301
+ calculateChecksum(migration) {
2302
+ const content = `${migration.name}:${migration.up.toString()}:${migration.down.toString()}`;
2303
+ return createHash("sha256").update(content).digest("hex");
2304
+ }
2305
+ };
2306
+
2307
+ // src/cli/utils.ts
2308
+ async function createAdapterFromConfig(config) {
2309
+ const { dialect, host, port, user, password, database, ssl } = config.connection;
2310
+ if (dialect === "mysql") {
2311
+ try {
2312
+ const { MySQLAdapter } = await import('@db-bridge/mysql');
2313
+ const adapter = new MySQLAdapter();
2314
+ await adapter.connect({
2315
+ host,
2316
+ port,
2317
+ user,
2318
+ password,
2319
+ database
2320
+ });
2321
+ return adapter;
2322
+ } catch (error2) {
2323
+ if (error2.message.includes("Cannot find module")) {
2324
+ throw new Error(
2325
+ "MySQL adapter not found. Install it with: npm install @db-bridge/mysql mysql2"
2326
+ );
2327
+ }
2328
+ throw error2;
2329
+ }
2330
+ }
2331
+ if (dialect === "postgresql") {
2332
+ try {
2333
+ const { PostgreSQLAdapter } = await import('@db-bridge/postgresql');
2334
+ const adapter = new PostgreSQLAdapter();
2335
+ await adapter.connect({
2336
+ host,
2337
+ port,
2338
+ user,
2339
+ password,
2340
+ database,
2341
+ ssl
2342
+ });
2343
+ return adapter;
2344
+ } catch (error2) {
2345
+ if (error2.message.includes("Cannot find module")) {
2346
+ throw new Error(
2347
+ "PostgreSQL adapter not found. Install it with: npm install @db-bridge/postgresql pg"
2348
+ );
2349
+ }
2350
+ throw error2;
2351
+ }
2352
+ }
2353
+ throw new Error(`Unsupported dialect: ${dialect}`);
2354
+ }
2355
+ async function createRunnerFromConfig(options = {}) {
2356
+ const config = await loadConfig();
2357
+ const adapter = await createAdapterFromConfig(config);
2358
+ const runner = new FileMigrationRunner(adapter, {
2359
+ directory: resolve(process.cwd(), config.migrations?.directory || "./src/migrations"),
2360
+ tableName: config.migrations?.tableName,
2361
+ lockTableName: config.migrations?.lockTableName,
2362
+ dialect: config.connection.dialect,
2363
+ dryRun: options.dryRun
2364
+ });
2365
+ return { runner, adapter, config };
2366
+ }
2367
+ function formatTable(headers, rows) {
2368
+ const widths = headers.map((h, i) => {
2369
+ const maxRow = Math.max(...rows.map((r) => (r[i] || "").length));
2370
+ return Math.max(h.length, maxRow);
2371
+ });
2372
+ const separator = "\u2500".repeat(widths.reduce((a, b) => a + b + 3, 1));
2373
+ const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(" \u2502 ");
2374
+ const dataRows = rows.map((row) => row.map((cell, i) => (cell || "").padEnd(widths[i])).join(" \u2502 ")).join("\n");
2375
+ return `\u250C${separator}\u2510
2376
+ \u2502 ${headerRow} \u2502
2377
+ \u251C${separator}\u2524
2378
+ ${dataRows ? dataRows.split("\n").map((r) => `\u2502 ${r} \u2502`).join("\n") : `\u2502${" ".repeat(separator.length)}\u2502`}
2379
+ \u2514${separator}\u2518`;
2380
+ }
2381
+ function success(message) {
2382
+ console.log(`\u2713 ${message}`);
2383
+ }
2384
+ function error(message) {
2385
+ console.error(`\u2717 ${message}`);
2386
+ }
2387
+ function info(message) {
2388
+ console.log(`\u2139 ${message}`);
2389
+ }
2390
+ function warn(message) {
2391
+ console.warn(`\u26A0 ${message}`);
2392
+ }
2393
+
2394
+ // src/cli/commands/fresh.ts
2395
+ async function freshCommand(options = {}) {
2396
+ let adapter;
2397
+ try {
2398
+ if (options.dryRun) {
2399
+ info("DRY RUN MODE - No changes will be made");
2400
+ } else {
2401
+ warn("This will reset ALL migrations and re-run them!");
2402
+ }
2403
+ const { runner, adapter: a } = await createRunnerFromConfig(options);
2404
+ adapter = a;
2405
+ console.log("");
2406
+ info("Resetting all migrations...");
2407
+ const executed = await runner.fresh();
2408
+ if (executed.length === 0) {
2409
+ info("No migrations to run");
2410
+ } else {
2411
+ console.log("");
2412
+ success(`Fresh migration complete - ran ${executed.length} migration(s):`);
2413
+ for (const name of executed) {
2414
+ console.log(` - ${name}`);
2415
+ }
2416
+ }
2417
+ } catch (error_) {
2418
+ error(error_.message);
2419
+ process.exit(1);
2420
+ } finally {
2421
+ if (adapter) {
2422
+ await adapter.disconnect();
2423
+ }
2424
+ }
2425
+ }
2426
+
2427
+ // src/cli/commands/latest.ts
2428
+ async function latestCommand(options = {}) {
2429
+ let adapter;
2430
+ try {
2431
+ if (options.dryRun) {
2432
+ info("DRY RUN MODE - No changes will be made");
2433
+ }
2434
+ const { runner, adapter: a } = await createRunnerFromConfig(options);
2435
+ adapter = a;
2436
+ const executed = await runner.latest();
2437
+ if (executed.length === 0) {
2438
+ info("Nothing to migrate");
2439
+ } else {
2440
+ console.log("");
2441
+ success(`Ran ${executed.length} migration(s):`);
2442
+ for (const name of executed) {
2443
+ console.log(` - ${name}`);
2444
+ }
2445
+ }
2446
+ } catch (error_) {
2447
+ error(error_.message);
2448
+ process.exit(1);
2449
+ } finally {
2450
+ if (adapter) {
2451
+ await adapter.disconnect();
2452
+ }
2453
+ }
2454
+ }
2455
+ async function makeCommand(name) {
2456
+ try {
2457
+ const config = await loadConfig();
2458
+ const directory = resolve(process.cwd(), config.migrations?.directory || "./src/migrations");
2459
+ if (!existsSync(directory)) {
2460
+ await mkdir(directory, { recursive: true });
2461
+ console.log(`Created migrations directory: ${directory}`);
2462
+ }
2463
+ const filename = MigrationLoader.generateFilename(name);
2464
+ const filepath = resolve(directory, filename);
2465
+ const migrationName = filename.replace(/\.(ts|js|mjs)$/, "");
2466
+ const content = MigrationLoader.getMigrationTemplate(migrationName);
2467
+ if (existsSync(filepath)) {
2468
+ error(`Migration file already exists: ${filepath}`);
2469
+ process.exit(1);
2470
+ }
2471
+ await writeFile(filepath, content, "utf8");
2472
+ success(`Created migration: ${filename}`);
2473
+ console.log(` Path: ${filepath}`);
2474
+ } catch (error_) {
2475
+ error(error_.message);
2476
+ process.exit(1);
2477
+ }
2478
+ }
2479
+ var SeederLoader = class {
2480
+ constructor(directory) {
2481
+ this.directory = directory;
2482
+ }
2483
+ /**
2484
+ * Load all seeder files from directory
2485
+ */
2486
+ async loadAll() {
2487
+ const files = await readdir(this.directory);
2488
+ const seederFiles = files.filter((f) => /\.(ts|js|mjs)$/.test(f) && !f.endsWith(".d.ts")).sort().map((f) => ({
2489
+ name: this.getSeederName(f),
2490
+ path: resolve(this.directory, f)
2491
+ }));
2492
+ return seederFiles;
2493
+ }
2494
+ /**
2495
+ * Load a specific seeder by path
2496
+ */
2497
+ async load(seederPath) {
2498
+ const fileUrl = pathToFileURL(seederPath).href;
2499
+ const module = await import(fileUrl);
2500
+ const seeder = module.default || module;
2501
+ if (!seeder || typeof seeder.run !== "function") {
2502
+ throw new Error(`Invalid seeder: ${seederPath} - must export a run() function`);
2503
+ }
2504
+ return seeder;
2505
+ }
2506
+ /**
2507
+ * Get seeder name from filename
2508
+ */
2509
+ getSeederName(filename) {
2510
+ return basename(filename).replace(/\.(ts|js|mjs)$/, "");
2511
+ }
2512
+ /**
2513
+ * Generate a new seeder filename
2514
+ */
2515
+ static generateFilename(name) {
2516
+ const snakeName = name.replaceAll(/([a-z])([A-Z])/g, "$1_$2").replaceAll(/[\s-]+/g, "_").toLowerCase();
2517
+ return `${snakeName}_seeder.ts`;
2518
+ }
2519
+ /**
2520
+ * Get seeder template
2521
+ */
2522
+ static getSeederTemplate(name) {
2523
+ const className = name.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
2524
+ return `/**
2525
+ * ${className} Seeder
2526
+ */
2527
+
2528
+ import type { Seeder, DatabaseAdapter } from '@db-bridge/core';
2529
+
2530
+ export default {
2531
+ async run(adapter: DatabaseAdapter): Promise<void> {
2532
+ // Insert seed data
2533
+ // await adapter.execute(\`
2534
+ // INSERT INTO users (name, email) VALUES
2535
+ // ('John Doe', 'john@example.com'),
2536
+ // ('Jane Doe', 'jane@example.com')
2537
+ // \`);
2538
+ },
2539
+ } satisfies Seeder;
2540
+ `;
2541
+ }
2542
+ };
2543
+
2544
+ // src/cli/commands/make-seeder.ts
2545
+ async function makeSeederCommand(name) {
2546
+ try {
2547
+ const config = await loadConfig();
2548
+ const directory = resolve(process.cwd(), config.seeds?.directory || "./src/seeds");
2549
+ if (!existsSync(directory)) {
2550
+ await mkdir(directory, { recursive: true });
2551
+ console.log(`Created seeds directory: ${directory}`);
2552
+ }
2553
+ const filename = SeederLoader.generateFilename(name);
2554
+ const filepath = resolve(directory, filename);
2555
+ const seederName = filename.replace(/\.(ts|js|mjs)$/, "");
2556
+ const content = SeederLoader.getSeederTemplate(seederName);
2557
+ if (existsSync(filepath)) {
2558
+ error(`Seeder file already exists: ${filepath}`);
2559
+ process.exit(1);
2560
+ }
2561
+ await writeFile(filepath, content, "utf8");
2562
+ success(`Created seeder: ${filename}`);
2563
+ console.log(` Path: ${filepath}`);
2564
+ } catch (error_) {
2565
+ error(error_.message);
2566
+ process.exit(1);
2567
+ }
2568
+ }
2569
+
2570
+ // src/cli/commands/reset.ts
2571
+ async function resetCommand(options = {}) {
2572
+ let adapter;
2573
+ try {
2574
+ if (options.dryRun) {
2575
+ info("DRY RUN MODE - No changes will be made");
2576
+ } else {
2577
+ warn("This will rollback ALL migrations!");
2578
+ }
2579
+ const { runner, adapter: a } = await createRunnerFromConfig(options);
2580
+ adapter = a;
2581
+ const rolledBack = await runner.reset();
2582
+ if (rolledBack.length === 0) {
2583
+ info("Nothing to reset");
2584
+ } else {
2585
+ console.log("");
2586
+ success(`Reset ${rolledBack.length} migration(s):`);
2587
+ for (const name of rolledBack) {
2588
+ console.log(` - ${name}`);
2589
+ }
2590
+ }
2591
+ } catch (error_) {
2592
+ error(error_.message);
2593
+ process.exit(1);
2594
+ } finally {
2595
+ if (adapter) {
2596
+ await adapter.disconnect();
2597
+ }
2598
+ }
2599
+ }
2600
+
2601
+ // src/cli/commands/rollback.ts
2602
+ async function rollbackCommand(options = {}) {
2603
+ let adapter;
2604
+ try {
2605
+ if (options.dryRun) {
2606
+ info("DRY RUN MODE - No changes will be made");
2607
+ }
2608
+ const { runner, adapter: a } = await createRunnerFromConfig(options);
2609
+ adapter = a;
2610
+ const step = options.step ?? 1;
2611
+ const rolledBack = await runner.rollback(step);
2612
+ if (rolledBack.length === 0) {
2613
+ info("Nothing to rollback");
2614
+ } else {
2615
+ console.log("");
2616
+ success(`Rolled back ${rolledBack.length} migration(s):`);
2617
+ for (const name of rolledBack) {
2618
+ console.log(` - ${name}`);
2619
+ }
2620
+ }
2621
+ } catch (error_) {
2622
+ error(error_.message);
2623
+ process.exit(1);
2624
+ } finally {
2625
+ if (adapter) {
2626
+ await adapter.disconnect();
2627
+ }
2628
+ }
2629
+ }
2630
+
2631
+ // src/seeds/SeederRunner.ts
2632
+ var SeederRunner = class {
2633
+ constructor(adapter, options) {
2634
+ this.adapter = adapter;
2635
+ this.options = options;
2636
+ this.loader = new SeederLoader(options.directory);
2637
+ }
2638
+ loader;
2639
+ options;
2640
+ /**
2641
+ * Run all seeders (or filtered by options)
2642
+ */
2643
+ async run() {
2644
+ const files = await this.loader.loadAll();
2645
+ const filteredFiles = this.filterSeeders(files);
2646
+ const results = [];
2647
+ for (const file of filteredFiles) {
2648
+ const startTime = Date.now();
2649
+ try {
2650
+ const seeder = await this.loader.load(file.path);
2651
+ await seeder.run(this.adapter);
2652
+ results.push({
2653
+ name: file.name,
2654
+ success: true,
2655
+ duration: Date.now() - startTime
2656
+ });
2657
+ } catch (error2) {
2658
+ results.push({
2659
+ name: file.name,
2660
+ success: false,
2661
+ error: error2.message,
2662
+ duration: Date.now() - startTime
2663
+ });
2664
+ break;
2665
+ }
2666
+ }
2667
+ return results;
2668
+ }
2669
+ /**
2670
+ * Run a specific seeder by name
2671
+ */
2672
+ async runSeeder(name) {
2673
+ const files = await this.loader.loadAll();
2674
+ const file = files.find((f) => f.name === name || f.name === `${name}_seeder`);
2675
+ if (!file) {
2676
+ return {
2677
+ name,
2678
+ success: false,
2679
+ error: `Seeder not found: ${name}`,
2680
+ duration: 0
2681
+ };
2682
+ }
2683
+ const startTime = Date.now();
2684
+ try {
2685
+ const seeder = await this.loader.load(file.path);
2686
+ await seeder.run(this.adapter);
2687
+ return {
2688
+ name: file.name,
2689
+ success: true,
2690
+ duration: Date.now() - startTime
2691
+ };
2692
+ } catch (error2) {
2693
+ return {
2694
+ name: file.name,
2695
+ success: false,
2696
+ error: error2.message,
2697
+ duration: Date.now() - startTime
2698
+ };
2699
+ }
2700
+ }
2701
+ /**
2702
+ * Filter seeders based on options
2703
+ */
2704
+ filterSeeders(files) {
2705
+ let filtered = files;
2706
+ if (this.options.only && this.options.only.length > 0) {
2707
+ filtered = filtered.filter(
2708
+ (f) => this.options.only.some((name) => f.name === name || f.name === `${name}_seeder`)
2709
+ );
2710
+ }
2711
+ if (this.options.except && this.options.except.length > 0) {
2712
+ filtered = filtered.filter(
2713
+ (f) => !this.options.except.some((name) => f.name === name || f.name === `${name}_seeder`)
2714
+ );
2715
+ }
2716
+ return filtered;
2717
+ }
2718
+ /**
2719
+ * List all available seeders
2720
+ */
2721
+ async list() {
2722
+ return this.loader.loadAll();
2723
+ }
2724
+ };
2725
+
2726
+ // src/cli/commands/seed.ts
2727
+ async function seedCommand(options = {}) {
2728
+ let adapter;
2729
+ try {
2730
+ const config = await loadConfig();
2731
+ adapter = await createAdapterFromConfig(config);
2732
+ const directory = resolve(process.cwd(), config.seeds?.directory || "./src/seeds");
2733
+ const runner = new SeederRunner(adapter, { directory });
2734
+ if (options.class) {
2735
+ info(`Running seeder: ${options.class}`);
2736
+ const result = await runner.runSeeder(options.class);
2737
+ if (result.success) {
2738
+ success(`Seeder completed: ${result.name} (${result.duration}ms)`);
2739
+ } else {
2740
+ error(`Seeder failed: ${result.error}`);
2741
+ process.exit(1);
2742
+ }
2743
+ } else {
2744
+ info("Running all seeders...");
2745
+ console.log("");
2746
+ const results = await runner.run();
2747
+ if (results.length === 0) {
2748
+ info("No seeders found");
2749
+ return;
2750
+ }
2751
+ let hasErrors = false;
2752
+ for (const result of results) {
2753
+ if (result.success) {
2754
+ success(`${result.name} (${result.duration}ms)`);
2755
+ } else {
2756
+ error(`${result.name}: ${result.error}`);
2757
+ hasErrors = true;
2758
+ }
2759
+ }
2760
+ console.log("");
2761
+ if (hasErrors) {
2762
+ error("Seeding completed with errors");
2763
+ process.exit(1);
2764
+ } else {
2765
+ success(`Seeding complete - ran ${results.length} seeder(s)`);
2766
+ }
2767
+ }
2768
+ } catch (error_) {
2769
+ error(error_.message);
2770
+ process.exit(1);
2771
+ } finally {
2772
+ if (adapter) {
2773
+ await adapter.disconnect();
2774
+ }
2775
+ }
2776
+ }
2777
+
2778
+ // src/cli/commands/status.ts
2779
+ async function statusCommand() {
2780
+ let adapter;
2781
+ try {
2782
+ const { runner, adapter: a } = await createRunnerFromConfig();
2783
+ adapter = a;
2784
+ const status = await runner.status();
2785
+ if (status.length === 0) {
2786
+ console.log("No migrations found");
2787
+ return;
2788
+ }
2789
+ const headers = ["Migration", "Batch", "Status", "Executed At"];
2790
+ const rows = status.map((s) => [
2791
+ s.name,
2792
+ s.batch?.toString() || "-",
2793
+ s.pending ? "Pending" : "Executed",
2794
+ s.executedAt ? new Date(s.executedAt).toLocaleString() : "-"
2795
+ ]);
2796
+ console.log("");
2797
+ console.log("Migration Status:");
2798
+ console.log("");
2799
+ console.log(formatTable(headers, rows));
2800
+ console.log("");
2801
+ const pendingCount = status.filter((s) => s.pending).length;
2802
+ const executedCount = status.filter((s) => !s.pending).length;
2803
+ console.log(
2804
+ `Total: ${status.length} migrations (${executedCount} executed, ${pendingCount} pending)`
2805
+ );
2806
+ } catch (error_) {
2807
+ error(error_.message);
2808
+ process.exit(1);
2809
+ } finally {
2810
+ if (adapter) {
2811
+ await adapter.disconnect();
2812
+ }
2813
+ }
2814
+ }
2815
+
2816
+ // src/cli/commands/validate.ts
2817
+ async function validateCommand() {
2818
+ let adapter;
2819
+ try {
2820
+ info("Validating migrations...");
2821
+ console.log("");
2822
+ const { runner, adapter: a } = await createRunnerFromConfig();
2823
+ adapter = a;
2824
+ const result = await runner.validate();
2825
+ if (result.valid) {
2826
+ success("All migrations are valid");
2827
+ } else {
2828
+ warn("Migration validation failed:");
2829
+ console.log("");
2830
+ for (const err of result.errors) {
2831
+ error(` ${err}`);
2832
+ }
2833
+ console.log("");
2834
+ process.exit(1);
2835
+ }
2836
+ } catch (error_) {
2837
+ error(error_.message);
2838
+ process.exit(1);
2839
+ } finally {
2840
+ if (adapter) {
2841
+ await adapter.disconnect();
2842
+ }
2843
+ }
2844
+ }
2845
+
2846
+ // src/cli/index.ts
2847
+ var VERSION = "1.0.0";
2848
+ var HELP = `
2849
+ db-bridge - Database Migration & Seeding CLI
2850
+
2851
+ Usage:
2852
+ db-bridge <command> [options]
2853
+
2854
+ Migration Commands:
2855
+ migrate:make <name> Create a new migration file
2856
+ migrate:latest Run all pending migrations
2857
+ migrate:rollback Rollback the last batch of migrations
2858
+ migrate:status Show migration status
2859
+ migrate:reset Rollback all migrations
2860
+ migrate:fresh Drop all tables and re-run migrations
2861
+ migrate:validate Validate migration checksums
2862
+
2863
+ Seed Commands:
2864
+ make:seeder <name> Create a new seeder file
2865
+ db:seed Run database seeders
2866
+ db:seed --class=<name> Run a specific seeder
2867
+
2868
+ Options:
2869
+ --help, -h Show this help message
2870
+ --version, -v Show version number
2871
+ --dry-run Show what would be done without executing
2872
+ --step=<n> Number of batches to rollback (for rollback command)
2873
+ --class=<name> Specific seeder class to run (for db:seed)
2874
+
2875
+ Examples:
2876
+ db-bridge migrate:make create_users_table
2877
+ db-bridge migrate:latest
2878
+ db-bridge migrate:rollback --step=2
2879
+ db-bridge make:seeder users
2880
+ db-bridge db:seed
2881
+ db-bridge db:seed --class=users
2882
+ `;
2883
+ async function main() {
2884
+ const args = process.argv.slice(2);
2885
+ let options = {};
2886
+ let command = "";
2887
+ let commandArgs = [];
2888
+ try {
2889
+ const { values, positionals } = parseArgs({
2890
+ args,
2891
+ options: {
2892
+ help: { type: "boolean", short: "h" },
2893
+ version: { type: "boolean", short: "v" },
2894
+ "dry-run": { type: "boolean" },
2895
+ step: { type: "string" },
2896
+ class: { type: "string" }
2897
+ },
2898
+ allowPositionals: true
2899
+ });
2900
+ options = {
2901
+ help: values.help,
2902
+ version: values.version,
2903
+ dryRun: values["dry-run"],
2904
+ step: values.step ? parseInt(values.step, 10) : void 0,
2905
+ class: values.class
2906
+ };
2907
+ command = positionals[0] || "";
2908
+ commandArgs = positionals.slice(1);
2909
+ } catch {
2910
+ command = args[0] || "";
2911
+ commandArgs = args.slice(1).filter((a) => !a.startsWith("-"));
2912
+ options.help = args.includes("--help") || args.includes("-h");
2913
+ options.version = args.includes("--version") || args.includes("-v");
2914
+ options.dryRun = args.includes("--dry-run");
2915
+ const stepArg = args.find((a) => a.startsWith("--step="));
2916
+ if (stepArg) {
2917
+ options.step = parseInt(stepArg.split("=")[1] || "1", 10);
2918
+ }
2919
+ const classArg = args.find((a) => a.startsWith("--class="));
2920
+ if (classArg) {
2921
+ options.class = classArg.split("=")[1];
2922
+ }
2923
+ }
2924
+ if (options.help || command === "help") {
2925
+ console.log(HELP);
2926
+ process.exit(0);
2927
+ }
2928
+ if (options.version) {
2929
+ console.log(`db-bridge v${VERSION}`);
2930
+ process.exit(0);
2931
+ }
2932
+ if (!command) {
2933
+ console.log(HELP);
2934
+ process.exit(0);
2935
+ }
2936
+ try {
2937
+ switch (command) {
2938
+ case "migrate:make": {
2939
+ if (!commandArgs[0]) {
2940
+ console.error("Error: Migration name is required");
2941
+ console.log("Usage: db-bridge migrate:make <name>");
2942
+ process.exit(1);
2943
+ }
2944
+ await makeCommand(commandArgs[0]);
2945
+ break;
2946
+ }
2947
+ case "migrate:latest": {
2948
+ await latestCommand({ dryRun: options.dryRun });
2949
+ break;
2950
+ }
2951
+ case "migrate:rollback": {
2952
+ await rollbackCommand({ dryRun: options.dryRun, step: options.step });
2953
+ break;
2954
+ }
2955
+ case "migrate:status": {
2956
+ await statusCommand();
2957
+ break;
2958
+ }
2959
+ case "migrate:reset": {
2960
+ await resetCommand({ dryRun: options.dryRun });
2961
+ break;
2962
+ }
2963
+ case "migrate:fresh": {
2964
+ await freshCommand({ dryRun: options.dryRun });
2965
+ break;
2966
+ }
2967
+ case "migrate:validate": {
2968
+ await validateCommand();
2969
+ break;
2970
+ }
2971
+ case "make:seeder": {
2972
+ if (!commandArgs[0]) {
2973
+ console.error("Error: Seeder name is required");
2974
+ console.log("Usage: db-bridge make:seeder <name>");
2975
+ process.exit(1);
2976
+ }
2977
+ await makeSeederCommand(commandArgs[0]);
2978
+ break;
2979
+ }
2980
+ case "db:seed": {
2981
+ await seedCommand({ class: options.class });
2982
+ break;
2983
+ }
2984
+ default: {
2985
+ console.error(`Unknown command: ${command}`);
2986
+ console.log('Run "db-bridge --help" for usage information.');
2987
+ process.exit(1);
2988
+ }
2989
+ }
2990
+ } catch (error2) {
2991
+ console.error(`Error: ${error2.message}`);
2992
+ process.exit(1);
2993
+ }
2994
+ }
2995
+ main().catch((error2) => {
2996
+ console.error("Fatal error:", error2);
2997
+ process.exit(1);
2998
+ });
2999
+ //# sourceMappingURL=index.js.map
3000
+ //# sourceMappingURL=index.js.map