forge-sql-orm-cli 1.0.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.
@@ -0,0 +1,659 @@
1
+ import "reflect-metadata";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import mysql from "mysql2/promise";
5
+ import { MySqlTable, TableConfig } from "drizzle-orm/mysql-core";
6
+ import { RowDataPacket } from "mysql2";
7
+ import { getTableMetadata } from "forge-sql-orm";
8
+ import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
9
+ import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
10
+ import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
11
+
12
+ interface DrizzleColumn {
13
+ type: string;
14
+ notNull: boolean;
15
+ autoincrement?: boolean;
16
+ columnType?: any;
17
+ name: string;
18
+ getSQLType: () => string;
19
+ }
20
+
21
+ interface DrizzleSchema {
22
+ [tableName: string]: {
23
+ [columnName: string]: DrizzleColumn;
24
+ };
25
+ }
26
+
27
+ interface DatabaseColumn extends RowDataPacket {
28
+ TABLE_NAME: string;
29
+ COLUMN_NAME: string;
30
+ COLUMN_TYPE: string;
31
+ IS_NULLABLE: string;
32
+ COLUMN_KEY: string;
33
+ EXTRA: string;
34
+ }
35
+
36
+ interface DatabaseIndex extends RowDataPacket {
37
+ TABLE_NAME: string;
38
+ INDEX_NAME: string;
39
+ COLUMN_NAME: string;
40
+ NON_UNIQUE: number;
41
+ }
42
+
43
+ interface DatabaseForeignKey extends RowDataPacket {
44
+ TABLE_NAME: string;
45
+ COLUMN_NAME: string;
46
+ CONSTRAINT_NAME: string;
47
+ REFERENCED_TABLE_NAME: string;
48
+ REFERENCED_COLUMN_NAME: string;
49
+ }
50
+
51
+ interface TableSchema {
52
+ columns: Record<string, DatabaseColumn>;
53
+ indexes: Record<
54
+ string,
55
+ {
56
+ columns: string[];
57
+ unique: boolean;
58
+ }
59
+ >;
60
+ foreignKeys: Record<
61
+ string,
62
+ {
63
+ column: string;
64
+ referencedTable: string;
65
+ referencedColumn: string;
66
+ }
67
+ >;
68
+ }
69
+
70
+ interface DatabaseSchema {
71
+ [tableName: string]: TableSchema;
72
+ }
73
+
74
+ /**
75
+ * Generates a migration file using the provided SQL statements.
76
+ * @param createStatements - Array of SQL statements.
77
+ * @param version - Migration version number.
78
+ * @returns TypeScript migration file content.
79
+ */
80
+ function generateMigrationFile(createStatements: string[], version: number): string {
81
+ const versionPrefix = `v${version}_MIGRATION`;
82
+
83
+ // Clean each SQL statement and generate migration lines with .enqueue()
84
+ const migrationLines = createStatements
85
+ .map((stmt, index) => ` .enqueue("${versionPrefix}${index}", "${stmt}")`)
86
+ .join("\n");
87
+
88
+ // Migration template
89
+ return `import { MigrationRunner } from "@forge/sql/out/migration";
90
+
91
+ export default (migrationRunner: MigrationRunner): MigrationRunner => {
92
+ return migrationRunner
93
+ ${migrationLines};
94
+ };`;
95
+ }
96
+
97
+ /**
98
+ * Filters out SQL statements that already exist in the previous migration file
99
+ * @param newStatements - Array of SQL statements from new migration
100
+ * @param prevVersion - Previous migration version
101
+ * @param outputDir - Directory where migration files are stored
102
+ * @returns Array of SQL statements that don't exist in previous migration
103
+ */
104
+ function filterWithPreviousMigration(
105
+ newStatements: string[],
106
+ prevVersion: number,
107
+ outputDir: string,
108
+ ): string[] {
109
+ const prevMigrationPath = path.join(outputDir, `migrationV${prevVersion}.ts`);
110
+
111
+ if (!fs.existsSync(prevMigrationPath)) {
112
+ return newStatements.map((s) => s.replace(/\s+/g, " "));
113
+ }
114
+
115
+ // Read previous migration file
116
+ const prevContent = fs.readFileSync(prevMigrationPath, "utf-8");
117
+
118
+ // Extract SQL statements from the file
119
+ const prevStatements = prevContent
120
+ .split("\n")
121
+ .filter((line) => line.includes(".enqueue("))
122
+ .map((line) => {
123
+ const match = line.match(/\.enqueue\([^,]+,\s*"([^"]+)"/);
124
+ return match ? match[1].replace(/\s+/g, " ").trim() : "";
125
+ });
126
+
127
+ // Filter out statements that already exist in previous migration
128
+ return newStatements
129
+ .filter((s) => !prevStatements.includes(s.replace(/\s+/g, " ")))
130
+ .map((s) => s.replace(/\s+/g, " "));
131
+ }
132
+
133
+ /**
134
+ * Saves the generated migration file along with `migrationCount.ts` and `index.ts`.
135
+ * @param migrationCode - The migration code to be written to the file.
136
+ * @param version - Migration version number.
137
+ * @param outputDir - Directory where the migration files will be saved.
138
+ * @returns boolean indicating if migration was saved
139
+ */
140
+ function saveMigrationFiles(migrationCode: string, version: number, outputDir: string): boolean {
141
+ if (!fs.existsSync(outputDir)) {
142
+ fs.mkdirSync(outputDir, { recursive: true });
143
+ }
144
+
145
+ const migrationFilePath = path.join(outputDir, `migrationV${version}.ts`);
146
+ const migrationCountPath = path.join(outputDir, `migrationCount.ts`);
147
+ const indexFilePath = path.join(outputDir, `index.ts`);
148
+
149
+ // Write the migration file
150
+ fs.writeFileSync(migrationFilePath, migrationCode);
151
+
152
+ // Write the migration count file
153
+ fs.writeFileSync(migrationCountPath, `export const MIGRATION_VERSION = ${version};`);
154
+
155
+ // Generate the migration index file
156
+ const indexFileContent = `import { MigrationRunner } from "@forge/sql/out/migration";
157
+ import { MIGRATION_VERSION } from "./migrationCount";
158
+
159
+ export type MigrationType = (
160
+ migrationRunner: MigrationRunner,
161
+ ) => MigrationRunner;
162
+
163
+ export default async (
164
+ migrationRunner: MigrationRunner,
165
+ ): Promise<MigrationRunner> => {
166
+ for (let i = 1; i <= MIGRATION_VERSION; i++) {
167
+ const migrations = (await import(\`./migrationV\${i}\`)) as {
168
+ default: MigrationType;
169
+ };
170
+ migrations.default(migrationRunner);
171
+ }
172
+ return migrationRunner;
173
+ };`;
174
+
175
+ fs.writeFileSync(indexFilePath, indexFileContent);
176
+
177
+ console.log(`✅ Migration file created: ${migrationFilePath}`);
178
+ console.log(`✅ Migration count file updated: ${migrationCountPath}`);
179
+ console.log(`✅ Migration index file created: ${indexFilePath}`);
180
+
181
+ return true;
182
+ }
183
+
184
+ /**
185
+ * Loads the current migration version from `migrationCount.ts`.
186
+ * @param migrationPath - Path to the migration folder.
187
+ * @returns The latest migration version.
188
+ */
189
+ const loadMigrationVersion = async (migrationPath: string): Promise<number> => {
190
+ try {
191
+ const migrationCountFilePath = path.resolve(path.join(migrationPath, "migrationCount.ts"));
192
+ if (!fs.existsSync(migrationCountFilePath)) {
193
+ console.warn(
194
+ `⚠️ Warning: migrationCount.ts not found in ${migrationCountFilePath}, assuming no previous migrations.`,
195
+ );
196
+ return 0;
197
+ }
198
+
199
+ const { MIGRATION_VERSION } = await import(migrationCountFilePath);
200
+ console.log(`✅ Current migration version: ${MIGRATION_VERSION}`);
201
+ return MIGRATION_VERSION as number;
202
+ } catch (error) {
203
+ console.error(`❌ Error loading migrationCount:`, error);
204
+ process.exit(1);
205
+ }
206
+ };
207
+
208
+ /**
209
+ * Gets the current database schema from MySQL including indexes and foreign keys
210
+ * @param connection - MySQL connection
211
+ * @param dbName - Database name
212
+ * @returns Database schema object with indexes and foreign keys
213
+ */
214
+ async function getDatabaseSchema(
215
+ connection: mysql.Connection,
216
+ dbName: string,
217
+ ): Promise<DatabaseSchema> {
218
+ // Get columns
219
+ const [columns] = await connection.execute<DatabaseColumn[]>(
220
+ `
221
+ SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA
222
+ FROM INFORMATION_SCHEMA.COLUMNS
223
+ WHERE TABLE_SCHEMA = ?
224
+ `,
225
+ [dbName],
226
+ );
227
+
228
+ // Get indexes
229
+ const [indexes] = await connection.execute<DatabaseIndex[]>(
230
+ `
231
+ SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, NON_UNIQUE
232
+ FROM INFORMATION_SCHEMA.STATISTICS
233
+ WHERE TABLE_SCHEMA = ?
234
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
235
+ `,
236
+ [dbName],
237
+ );
238
+
239
+ // Get foreign keys
240
+ const [foreignKeys] = await connection.execute<DatabaseForeignKey[]>(
241
+ `
242
+ SELECT
243
+ TABLE_NAME,
244
+ COLUMN_NAME,
245
+ CONSTRAINT_NAME,
246
+ REFERENCED_TABLE_NAME,
247
+ REFERENCED_COLUMN_NAME
248
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
249
+ WHERE TABLE_SCHEMA = ?
250
+ AND REFERENCED_TABLE_NAME IS NOT NULL
251
+ `,
252
+ [dbName],
253
+ );
254
+
255
+ const schema: DatabaseSchema = {};
256
+
257
+ // Process columns
258
+ columns.forEach((row) => {
259
+ if (!schema[row.TABLE_NAME]) {
260
+ schema[row.TABLE_NAME] = {
261
+ columns: {},
262
+ indexes: {},
263
+ foreignKeys: {},
264
+ };
265
+ }
266
+ schema[row.TABLE_NAME].columns[row.COLUMN_NAME] = row;
267
+ });
268
+
269
+ // Process indexes
270
+ indexes.forEach((row) => {
271
+ if (!schema[row.TABLE_NAME].indexes[row.INDEX_NAME]) {
272
+ schema[row.TABLE_NAME].indexes[row.INDEX_NAME] = {
273
+ columns: [],
274
+ unique: !row.NON_UNIQUE,
275
+ };
276
+ }
277
+ schema[row.TABLE_NAME].indexes[row.INDEX_NAME].columns.push(row.COLUMN_NAME);
278
+ });
279
+
280
+ // Process foreign keys
281
+ foreignKeys.forEach((row) => {
282
+ if (!schema[row.TABLE_NAME].foreignKeys[row.CONSTRAINT_NAME]) {
283
+ schema[row.TABLE_NAME].foreignKeys[row.CONSTRAINT_NAME] = {
284
+ column: row.COLUMN_NAME,
285
+ referencedTable: row.REFERENCED_TABLE_NAME,
286
+ referencedColumn: row.REFERENCED_COLUMN_NAME,
287
+ };
288
+ }
289
+ });
290
+
291
+ return schema;
292
+ }
293
+
294
+ /**
295
+ * Converts MySQL type to normalized format for comparison
296
+ * @param mysqlType - MySQL type from INFORMATION_SCHEMA or Drizzle type
297
+ * @returns Normalized type string
298
+ */
299
+ function normalizeMySQLType(mysqlType: string): string {
300
+ // Remove length/precision information
301
+ let normalized = mysqlType.replace(/\([^)]*\)/, "").toLowerCase();
302
+
303
+ // Remove 'mysql' prefix from Drizzle types
304
+ normalized = normalized.replace(/^mysql/, "");
305
+
306
+ return normalized;
307
+ }
308
+
309
+ /**
310
+ * Gets the name of a foreign key constraint
311
+ * @param fk - The foreign key builder
312
+ * @returns The name of the foreign key constraint
313
+ */
314
+ function getForeignKeyName(fk: ForeignKeyBuilder): string {
315
+ // @ts-ignore - Internal property access
316
+ return fk.name;
317
+ }
318
+
319
+ /**
320
+ * Gets the name of an index
321
+ * @param index - The index builder
322
+ * @returns The name of the index
323
+ */
324
+ function getIndexName(index: AnyIndexBuilder): string {
325
+ // @ts-ignore - Internal property access
326
+ return index.name;
327
+ }
328
+
329
+ /**
330
+ * Gets the name of a unique constraint
331
+ * @param uc - The unique constraint builder
332
+ * @returns The name of the unique constraint
333
+ */
334
+ function getUniqueConstraintName(uc: UniqueConstraintBuilder): string {
335
+ // @ts-ignore - Internal property access
336
+ return uc.name;
337
+ }
338
+
339
+ /**
340
+ * Gets the columns of an index
341
+ * @param index - The index builder
342
+ * @returns Array of column names
343
+ */
344
+ function getIndexColumns(index: AnyIndexBuilder): string[] {
345
+ // @ts-ignore - Internal property access
346
+ return index.columns.map((col) => col.name);
347
+ }
348
+
349
+ function compareForeignKey(
350
+ fk: ForeignKeyBuilder,
351
+ { columns }: { columns: string[]; unique: boolean },
352
+ ) {
353
+ // @ts-ignore
354
+ const fcolumns: string[] = fk.columns.map((c) => c.name);
355
+ return fcolumns.sort().join(",") === columns.sort().join(",");
356
+ }
357
+
358
+ /**
359
+ * Generates SQL changes by comparing Drizzle schema with database schema
360
+ * @param drizzleSchema - Schema from Drizzle
361
+ * @param dbSchema - Schema from database
362
+ * @param schemaModule - Drizzle schema module
363
+ * @returns Array of SQL statements
364
+ */
365
+ function generateSchemaChanges(
366
+ drizzleSchema: DrizzleSchema,
367
+ dbSchema: DatabaseSchema,
368
+ schemaModule: Record<string, any>,
369
+ ): string[] {
370
+ const changes: string[] = [];
371
+
372
+ // First check existing tables in database
373
+ for (const [tableName, dbTable] of Object.entries(dbSchema)) {
374
+ const drizzleColumns = drizzleSchema[tableName];
375
+
376
+ if (!drizzleColumns) {
377
+ // Table exists in database but not in schema - create it
378
+ const columns = Object.entries(dbTable.columns)
379
+ .map(([colName, col]) => {
380
+ const type = col.COLUMN_TYPE;
381
+ const nullable = col.IS_NULLABLE === "YES" ? "NULL" : "NOT NULL";
382
+ const autoIncrement = col.EXTRA.includes("auto_increment") ? "AUTO_INCREMENT" : "";
383
+ return `\`${colName}\` ${type} ${nullable} ${autoIncrement}`.trim();
384
+ })
385
+ .join(",\n ");
386
+
387
+ changes.push(`CREATE TABLE if not exists \`${tableName}\` (\n ${columns}\n);`);
388
+
389
+ // Create indexes for new table
390
+ for (const [indexName, dbIndex] of Object.entries(dbTable.indexes)) {
391
+ // Skip primary key and foreign key indexes
392
+ if (indexName === "PRIMARY") {
393
+ continue;
394
+ }
395
+
396
+ // Check if any column in this index is a foreign key
397
+ const isForeignKeyIndex = dbIndex.columns.some((colName) => {
398
+ const column = dbTable.columns[colName];
399
+ return column && column.COLUMN_KEY === "MUL" && column.EXTRA.includes("foreign key");
400
+ });
401
+
402
+ if (isForeignKeyIndex) {
403
+ continue;
404
+ }
405
+
406
+ // Create index
407
+ const columns = dbIndex.columns.map((col) => `\`${col}\``).join(", ");
408
+ const unique = dbIndex.unique ? "UNIQUE " : "";
409
+ changes.push(
410
+ `CREATE ${unique}INDEX if not exists \`${indexName}\` ON \`${tableName}\` (${columns});`,
411
+ );
412
+ }
413
+
414
+ // Create foreign keys for new table
415
+ for (const [fkName, dbFK] of Object.entries(dbTable.foreignKeys)) {
416
+ changes.push(
417
+ `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${fkName}\` FOREIGN KEY (\`${dbFK.column}\`) REFERENCES \`${dbFK.referencedTable}\` (\`${dbFK.referencedColumn}\`);`,
418
+ );
419
+ }
420
+ continue;
421
+ }
422
+
423
+ // Check for column changes in existing tables
424
+ for (const [colName, dbCol] of Object.entries(dbTable.columns)) {
425
+ const drizzleCol = Object.values(drizzleColumns).find((c) => c.name === colName);
426
+
427
+ if (!drizzleCol) {
428
+ // Column exists in database but not in schema - create it
429
+ const type = dbCol.COLUMN_TYPE;
430
+ const nullable = dbCol.IS_NULLABLE === "YES" ? "NULL" : "NOT NULL";
431
+ changes.push(`ALTER TABLE \`${tableName}\` ADD COLUMN \`${colName}\` ${type} ${nullable};`);
432
+ continue;
433
+ }
434
+
435
+ // Check for type changes
436
+ const normalizedDbType = normalizeMySQLType(dbCol.COLUMN_TYPE);
437
+ const normalizedDrizzleType = normalizeMySQLType(drizzleCol.getSQLType());
438
+
439
+ if (normalizedDbType !== normalizedDrizzleType) {
440
+ const type = dbCol.COLUMN_TYPE; // Use database type as source of truth
441
+ const nullable = dbCol.IS_NULLABLE === "YES" ? "NULL" : "NOT NULL";
442
+ changes.push(
443
+ `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${colName}\` ${type} ${nullable};`,
444
+ );
445
+ }
446
+ }
447
+
448
+ // Check for index changes
449
+ const table = Object.values(schemaModule).find((t) => {
450
+ const metadata = getTableMetadata(t);
451
+ return metadata.tableName === tableName;
452
+ });
453
+
454
+ if (table) {
455
+ const metadata = getTableMetadata(table);
456
+ // First check indexes that exist in database but not in schema
457
+ for (const [indexName, dbIndex] of Object.entries(dbTable.indexes)) {
458
+ // Skip primary key and foreign key indexes
459
+ if (indexName === "PRIMARY") {
460
+ continue;
461
+ }
462
+
463
+ // Check if this is a foreign key index
464
+ const isForeignKeyIndex = metadata.foreignKeys.some(
465
+ (fk) => getForeignKeyName(fk) === indexName || compareForeignKey(fk, dbIndex),
466
+ );
467
+ if (isForeignKeyIndex) {
468
+ continue;
469
+ }
470
+
471
+ // Check if this is a unique constraint
472
+ const existsUniqIndex = metadata.uniqueConstraints.find(
473
+ (uc) => getUniqueConstraintName(uc) === indexName,
474
+ );
475
+ let drizzleIndex = metadata.indexes.find((i) => getIndexName(i) === indexName);
476
+
477
+ if (!drizzleIndex && existsUniqIndex) {
478
+ drizzleIndex = existsUniqIndex as unknown as AnyIndexBuilder;
479
+ }
480
+
481
+ if (!drizzleIndex) {
482
+ // Index exists in database but not in schema - create it
483
+ const columns = dbIndex.columns.map((col) => `\`${col}\``).join(", ");
484
+ const unique = dbIndex.unique ? "UNIQUE " : "";
485
+ changes.push(
486
+ `CREATE ${unique}INDEX if not exists \`${indexName}\` ON \`${tableName}\` (${columns});`,
487
+ );
488
+ continue;
489
+ }
490
+
491
+ // Check if index columns changed
492
+ const dbColumns = dbIndex.columns.join(", ");
493
+ const drizzleColumns = getIndexColumns(drizzleIndex).join(", ");
494
+ if (
495
+ dbColumns !== drizzleColumns ||
496
+ dbIndex.unique !== drizzleIndex instanceof UniqueConstraintBuilder
497
+ ) {
498
+ // Drop and recreate index using database values
499
+ changes.push(`DROP INDEX \`${indexName}\` ON \`${tableName}\`;`);
500
+ const columns = dbIndex.columns.map((col) => `\`${col}\``).join(", ");
501
+ const unique = dbIndex.unique ? "UNIQUE " : "";
502
+ changes.push(
503
+ `CREATE ${unique}INDEX if not exists \`${indexName}\` ON \`${tableName}\` (${columns});`,
504
+ );
505
+ }
506
+ }
507
+
508
+ // First check foreign keys that exist in database but not in schema
509
+ for (const [fkName, dbFK] of Object.entries(dbTable.foreignKeys)) {
510
+ // Find if this column is referenced in Drizzle schema
511
+ const drizzleFK = metadata.foreignKeys.find(
512
+ (fk) =>
513
+ getForeignKeyName(fk) === fkName ||
514
+ compareForeignKey(fk, { columns: [dbFK.column], unique: false }),
515
+ );
516
+
517
+ if (!drizzleFK) {
518
+ // Foreign key exists in database but not in schema - drop it
519
+ changes.push(
520
+ `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${fkName}\` FOREIGN KEY (\`${dbFK.column}\`) REFERENCES \`${dbFK.referencedTable}\` (\`${dbFK.referencedColumn}\`);`,
521
+ );
522
+ continue;
523
+ }
524
+ }
525
+
526
+ // Then check for new foreign keys that exist in schema but not in database
527
+ for (const drizzleForeignKey of metadata.foreignKeys) {
528
+ // Find if this foreign key exists in database
529
+ const isDbFk = Object.keys(dbTable.foreignKeys).find((fk) => {
530
+ let foreignKey = dbTable.foreignKeys[fk];
531
+ return (
532
+ fk === getForeignKeyName(drizzleForeignKey) ||
533
+ compareForeignKey(drizzleForeignKey, { columns: [foreignKey.column], unique: false })
534
+ );
535
+ });
536
+
537
+ if (!isDbFk) {
538
+ // Foreign key exists in schema but not in database - create it
539
+ const fkName = getForeignKeyName(drizzleForeignKey);
540
+ if (fkName) {
541
+ changes.push(`ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${fkName}\`;`);
542
+ } else {
543
+ // @ts-ignore
544
+ const columns = drizzleForeignKey?.columns;
545
+ const columnNames = columns?.length
546
+ ? columns.map((c: any) => c.name).join(", ")
547
+ : "unknown columns";
548
+ console.warn(
549
+ `⚠️ Drizzle model for table '${tableName}' does not provide a name for FOREIGN KEY constraint on columns: ${columnNames}`,
550
+ );
551
+ }
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ return changes;
558
+ }
559
+
560
+ /**
561
+ * Updates an existing database migration by generating schema modifications.
562
+ * @param options - Database connection settings and output paths.
563
+ */
564
+ export const updateMigration = async (options: any) => {
565
+ try {
566
+ let version = await loadMigrationVersion(options.output);
567
+ const prevVersion = version;
568
+
569
+ if (version < 1) {
570
+ console.log(
571
+ `⚠️ Initial migration not found. Run "npx forge-sql-orm migrations:create" first.`,
572
+ );
573
+ process.exit(0);
574
+ }
575
+ version += 1;
576
+
577
+ // Create database connection
578
+ const connection = await mysql.createConnection({
579
+ host: options.host,
580
+ port: options.port,
581
+ user: options.user,
582
+ password: options.password,
583
+ database: options.dbName,
584
+ });
585
+
586
+ try {
587
+ // Get current database schema
588
+ const dbSchema = await getDatabaseSchema(connection, options.dbName);
589
+
590
+ // Import Drizzle schema using absolute path
591
+ const schemaPath = path.resolve(options.entitiesPath, "schema.ts");
592
+ if (!fs.existsSync(schemaPath)) {
593
+ throw new Error(`Schema file not found at: ${schemaPath}`);
594
+ }
595
+
596
+ const schemaModule = await import(schemaPath);
597
+ if (!schemaModule) {
598
+ throw new Error(`Invalid schema file at: ${schemaPath}. Schema must export tables.`);
599
+ }
600
+
601
+ // Process exported tables
602
+ const drizzleSchema: DrizzleSchema = {};
603
+
604
+ // Get all exports that are tables
605
+ const tables = Object.values(schemaModule) as MySqlTable<TableConfig>[];
606
+
607
+ tables.forEach((table) => {
608
+ const metadata = getTableMetadata(table);
609
+ if (metadata.tableName) {
610
+ // Convert AnyColumn to DrizzleColumn
611
+ const columns: Record<string, DrizzleColumn> = {};
612
+ Object.entries(metadata.columns).forEach(([name, column]) => {
613
+ columns[name] = {
614
+ type: column.dataType,
615
+ notNull: column.notNull,
616
+ autoincrement: (column as any).autoincrement,
617
+ columnType: column.columnType,
618
+ name: column.name,
619
+ getSQLType: () => column.getSQLType(),
620
+ };
621
+ });
622
+ drizzleSchema[metadata.tableName] = columns;
623
+ }
624
+ });
625
+
626
+ if (Object.keys(drizzleSchema).length === 0) {
627
+ throw new Error(`No valid tables found in schema at: ${schemaPath}`);
628
+ }
629
+
630
+ console.log("Found tables:", Object.keys(drizzleSchema));
631
+
632
+ // Generate SQL changes
633
+ const createStatements = filterWithPreviousMigration(
634
+ generateSchemaChanges(drizzleSchema, dbSchema, schemaModule),
635
+ prevVersion,
636
+ options.output,
637
+ );
638
+
639
+ if (createStatements.length) {
640
+ // Generate migration file content
641
+ const migrationFile = generateMigrationFile(createStatements, version);
642
+
643
+ // Save migration files only if there are actual changes
644
+ if (saveMigrationFiles(migrationFile, version, options.output)) {
645
+ console.log(`✅ Migration successfully updated!`);
646
+ }
647
+ process.exit(0);
648
+ } else {
649
+ console.log(`⚠️ No new migration changes detected.`);
650
+ process.exit(0);
651
+ }
652
+ } finally {
653
+ await connection.end();
654
+ }
655
+ } catch (error) {
656
+ console.error(`❌ Error during migration update:`, error);
657
+ process.exit(1);
658
+ }
659
+ };