@zyno-io/dk-server-foundation 26.412.2137 → 26.601.2229

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.
Files changed (124) hide show
  1. package/dist/resources/proto/generated/devconsole/devconsole.js +1 -1
  2. package/dist/src/app/config.d.ts +1 -0
  3. package/dist/src/app/config.d.ts.map +1 -1
  4. package/dist/src/app/config.js +1 -1
  5. package/dist/src/app/config.js.map +1 -1
  6. package/dist/src/database/common.d.ts +3 -0
  7. package/dist/src/database/common.d.ts.map +1 -1
  8. package/dist/src/database/common.js +46 -1
  9. package/dist/src/database/common.js.map +1 -1
  10. package/dist/src/database/factory.d.ts +17 -0
  11. package/dist/src/database/factory.d.ts.map +1 -0
  12. package/dist/src/database/factory.js +53 -0
  13. package/dist/src/database/factory.js.map +1 -0
  14. package/dist/src/database/index.d.ts +2 -0
  15. package/dist/src/database/index.d.ts.map +1 -1
  16. package/dist/src/database/index.js +2 -0
  17. package/dist/src/database/index.js.map +1 -1
  18. package/dist/src/database/migration/MigrationResetCommand.d.ts.map +1 -1
  19. package/dist/src/database/migration/MigrationResetCommand.js +6 -20
  20. package/dist/src/database/migration/MigrationResetCommand.js.map +1 -1
  21. package/dist/src/database/migration/MigrationRunCommand.d.ts.map +1 -1
  22. package/dist/src/database/migration/MigrationRunCommand.js +2 -0
  23. package/dist/src/database/migration/MigrationRunCommand.js.map +1 -1
  24. package/dist/src/database/migration/create/MigrationCreateCommand.d.ts +2 -0
  25. package/dist/src/database/migration/create/MigrationCreateCommand.d.ts.map +1 -1
  26. package/dist/src/database/migration/create/MigrationCreateCommand.js +36 -11
  27. package/dist/src/database/migration/create/MigrationCreateCommand.js.map +1 -1
  28. package/dist/src/database/migration/create/builder-regenerator.d.ts +12 -0
  29. package/dist/src/database/migration/create/builder-regenerator.d.ts.map +1 -0
  30. package/dist/src/database/migration/create/builder-regenerator.js +308 -0
  31. package/dist/src/database/migration/create/builder-regenerator.js.map +1 -0
  32. package/dist/src/database/migration/create/comparator.js +33 -4
  33. package/dist/src/database/migration/create/comparator.js.map +1 -1
  34. package/dist/src/database/migration/create/db-reader.js +63 -9
  35. package/dist/src/database/migration/create/db-reader.js.map +1 -1
  36. package/dist/src/database/migration/create/ddl-generator.d.ts.map +1 -1
  37. package/dist/src/database/migration/create/ddl-generator.js +44 -206
  38. package/dist/src/database/migration/create/ddl-generator.js.map +1 -1
  39. package/dist/src/database/migration/create/entity-reader.js +47 -5
  40. package/dist/src/database/migration/create/entity-reader.js.map +1 -1
  41. package/dist/src/database/migration/create/file-generator.d.ts +2 -0
  42. package/dist/src/database/migration/create/file-generator.d.ts.map +1 -1
  43. package/dist/src/database/migration/create/file-generator.js +7 -2
  44. package/dist/src/database/migration/create/file-generator.js.map +1 -1
  45. package/dist/src/database/migration/create/schema-model.d.ts +9 -0
  46. package/dist/src/database/migration/create/schema-model.d.ts.map +1 -1
  47. package/dist/src/database/migration/create/schema-model.js +3 -3
  48. package/dist/src/database/mysql.d.ts.map +1 -1
  49. package/dist/src/database/mysql.js +5 -11
  50. package/dist/src/database/mysql.js.map +1 -1
  51. package/dist/src/database/postgres.d.ts +1 -0
  52. package/dist/src/database/postgres.d.ts.map +1 -1
  53. package/dist/src/database/postgres.js +1 -13
  54. package/dist/src/database/postgres.js.map +1 -1
  55. package/dist/src/database/schema/AlterBlueprint.d.ts +50 -0
  56. package/dist/src/database/schema/AlterBlueprint.d.ts.map +1 -0
  57. package/dist/src/database/schema/AlterBlueprint.js +88 -0
  58. package/dist/src/database/schema/AlterBlueprint.js.map +1 -0
  59. package/dist/src/database/schema/Blueprint.d.ts +16 -0
  60. package/dist/src/database/schema/Blueprint.d.ts.map +1 -0
  61. package/dist/src/database/schema/Blueprint.js +51 -0
  62. package/dist/src/database/schema/Blueprint.js.map +1 -0
  63. package/dist/src/database/schema/BlueprintBase.d.ts +65 -0
  64. package/dist/src/database/schema/BlueprintBase.d.ts.map +1 -0
  65. package/dist/src/database/schema/BlueprintBase.js +385 -0
  66. package/dist/src/database/schema/BlueprintBase.js.map +1 -0
  67. package/dist/src/database/schema/ColumnDefinition.d.ts +43 -0
  68. package/dist/src/database/schema/ColumnDefinition.d.ts.map +1 -0
  69. package/dist/src/database/schema/ColumnDefinition.js +117 -0
  70. package/dist/src/database/schema/ColumnDefinition.js.map +1 -0
  71. package/dist/src/database/schema/Schema.d.ts +32 -0
  72. package/dist/src/database/schema/Schema.d.ts.map +1 -0
  73. package/dist/src/database/schema/Schema.js +151 -0
  74. package/dist/src/database/schema/Schema.js.map +1 -0
  75. package/dist/src/database/schema/grammar/Grammar.d.ts +35 -0
  76. package/dist/src/database/schema/grammar/Grammar.d.ts.map +1 -0
  77. package/dist/src/database/schema/grammar/Grammar.js +36 -0
  78. package/dist/src/database/schema/grammar/Grammar.js.map +1 -0
  79. package/dist/src/database/schema/grammar/MySQLGrammar.d.ts +29 -0
  80. package/dist/src/database/schema/grammar/MySQLGrammar.d.ts.map +1 -0
  81. package/dist/src/database/schema/grammar/MySQLGrammar.js +199 -0
  82. package/dist/src/database/schema/grammar/MySQLGrammar.js.map +1 -0
  83. package/dist/src/database/schema/grammar/PostgresGrammar.d.ts +29 -0
  84. package/dist/src/database/schema/grammar/PostgresGrammar.d.ts.map +1 -0
  85. package/dist/src/database/schema/grammar/PostgresGrammar.js +233 -0
  86. package/dist/src/database/schema/grammar/PostgresGrammar.js.map +1 -0
  87. package/dist/src/database/schema/index.d.ts +9 -0
  88. package/dist/src/database/schema/index.d.ts.map +1 -0
  89. package/dist/src/database/schema/index.js +12 -0
  90. package/dist/src/database/schema/index.js.map +1 -0
  91. package/dist/src/database/types.d.ts +8 -0
  92. package/dist/src/database/types.d.ts.map +1 -1
  93. package/dist/src/database/types.js +3 -1
  94. package/dist/src/database/types.js.map +1 -1
  95. package/docs/content/cli.md +4 -1
  96. package/docs/content/configuration.md +6 -0
  97. package/docs/content/database.md +137 -0
  98. package/package.json +1 -1
  99. package/src/app/config.ts +2 -0
  100. package/src/database/common.ts +51 -0
  101. package/src/database/factory.ts +59 -0
  102. package/src/database/index.ts +2 -0
  103. package/src/database/migration/MigrationResetCommand.ts +6 -22
  104. package/src/database/migration/MigrationRunCommand.ts +2 -0
  105. package/src/database/migration/create/MigrationCreateCommand.ts +42 -12
  106. package/src/database/migration/create/builder-regenerator.ts +294 -0
  107. package/src/database/migration/create/comparator.ts +34 -4
  108. package/src/database/migration/create/db-reader.ts +60 -9
  109. package/src/database/migration/create/ddl-generator.ts +45 -212
  110. package/src/database/migration/create/entity-reader.ts +46 -5
  111. package/src/database/migration/create/file-generator.ts +5 -1
  112. package/src/database/migration/create/schema-model.ts +6 -0
  113. package/src/database/mysql.ts +7 -15
  114. package/src/database/postgres.ts +3 -17
  115. package/src/database/schema/AlterBlueprint.ts +92 -0
  116. package/src/database/schema/Blueprint.ts +43 -0
  117. package/src/database/schema/BlueprintBase.ts +425 -0
  118. package/src/database/schema/ColumnDefinition.ts +131 -0
  119. package/src/database/schema/Schema.ts +167 -0
  120. package/src/database/schema/grammar/Grammar.ts +62 -0
  121. package/src/database/schema/grammar/MySQLGrammar.ts +206 -0
  122. package/src/database/schema/grammar/PostgresGrammar.ts +243 -0
  123. package/src/database/schema/index.ts +8 -0
  124. package/src/database/types.ts +6 -0
@@ -0,0 +1,59 @@
1
+ import { ClassType } from '@deepkit/core';
2
+ import { Database } from '@deepkit/orm';
3
+
4
+ import { BaseDatabase } from './common';
5
+ import { Dialect } from './dialect';
6
+ import { createMySQLDatabase, IMySQLDatabaseAdapterConfig, MySQLDatabaseAdapter } from './mysql';
7
+ import { createPostgresDatabase, IPostgresDatabaseAdapterConfig, PostgresDatabaseAdapter } from './postgres';
8
+
9
+ export type AnyDatabaseAdapter = MySQLDatabaseAdapter | PostgresDatabaseAdapter;
10
+
11
+ export interface ICommonDatabaseAdapterConfig {
12
+ enableLocksTable?: boolean;
13
+ }
14
+
15
+ type Entities = ConstructorParameters<typeof Database>[1];
16
+
17
+ export function createDatabase(config: ICommonDatabaseAdapterConfig, entities?: Entities): ClassType<BaseDatabase<AnyDatabaseAdapter>>;
18
+ export function createDatabase(
19
+ dialect: 'mysql',
20
+ config: IMySQLDatabaseAdapterConfig,
21
+ entities?: Entities
22
+ ): ClassType<BaseDatabase<MySQLDatabaseAdapter>>;
23
+ export function createDatabase(
24
+ dialect: 'postgres',
25
+ config: IPostgresDatabaseAdapterConfig,
26
+ entities?: Entities
27
+ ): ClassType<BaseDatabase<PostgresDatabaseAdapter>>;
28
+ export function createDatabase(
29
+ dialectOrConfig: Dialect | ICommonDatabaseAdapterConfig,
30
+ configOrEntities?: IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig | Entities,
31
+ maybeEntities?: Entities
32
+ ): ClassType<BaseDatabase<AnyDatabaseAdapter>> {
33
+ let dialect: Dialect;
34
+ let config: IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig;
35
+ let entities: Entities;
36
+
37
+ if (typeof dialectOrConfig === 'string') {
38
+ dialect = dialectOrConfig;
39
+ config = configOrEntities as IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig;
40
+ entities = maybeEntities ?? [];
41
+ } else {
42
+ dialect = resolveDialectFromEnv();
43
+ config = dialectOrConfig;
44
+ entities = (configOrEntities as Entities | undefined) ?? [];
45
+ }
46
+
47
+ return dialect === 'postgres'
48
+ ? createPostgresDatabase(config as IPostgresDatabaseAdapterConfig, entities)
49
+ : createMySQLDatabase(config as IMySQLDatabaseAdapterConfig, entities);
50
+ }
51
+
52
+ function resolveDialectFromEnv(): Dialect {
53
+ const value = process.env.DB_ADAPTER;
54
+ if (value === 'mysql' || value === 'postgres') return value;
55
+ throw new Error(
56
+ `createDatabase(sharedConfig) requires the DB_ADAPTER env var to be 'mysql' or 'postgres' (got: ${value ?? 'undefined'}). ` +
57
+ `Either set DB_ADAPTER or call createDatabase(dialect, config) explicitly.`
58
+ );
59
+ }
@@ -1,7 +1,9 @@
1
1
  export * from './common';
2
2
  export * from './dialect';
3
3
  export * from './entity';
4
+ export * from './factory';
4
5
  export { createMigration, runMigrations, standardizeDbCollation } from './migration';
6
+ export * from './schema';
5
7
  export * from './mysql';
6
8
  export * from './postgres';
7
9
  export * from './types';
@@ -3,14 +3,11 @@ import { SQLDatabaseAdapter } from '@deepkit/sql';
3
3
  import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
4
4
  import path from 'path';
5
5
 
6
- import { getAppConfig } from '../../app/resolver';
7
6
  import { DBProvider } from '../../app/state';
8
7
  import { createLogger } from '../../services';
9
8
  import { getDialect } from '../dialect';
10
- import { generateDDL } from './create/ddl-generator';
9
+ import { generateBuilderMigrationFile } from './create/builder-regenerator';
11
10
  import { readEntitiesSchema } from './create/entity-reader';
12
- import { buildFileContent } from './create/file-generator';
13
- import { SchemaDiff } from './create/schema-model';
14
11
  import { getSourceMigrationsDir } from './helpers';
15
12
 
16
13
  @cli.controller('migration:reset')
@@ -21,13 +18,11 @@ export class MigrationResetCommand {
21
18
  constructor(private dbProvider: DBProvider) {}
22
19
 
23
20
  async execute() {
24
- // Step 1: Ensure migrations directory exists
25
21
  if (!existsSync(this.migrationsDir)) {
26
22
  this.logger.info(`Creating migrations directory: ${this.migrationsDir}`);
27
23
  mkdirSync(this.migrationsDir, { recursive: true });
28
24
  }
29
25
 
30
- // Step 2: Remove all .ts files from migrations directory
31
26
  const files = readdirSync(this.migrationsDir).filter(f => f.endsWith('.ts'));
32
27
  this.logger.info(`Removing ${files.length} migration file(s)`);
33
28
  for (const file of files) {
@@ -36,33 +31,22 @@ export class MigrationResetCommand {
36
31
  this.logger.info(`Removed ${file}`);
37
32
  }
38
33
 
39
- // Step 3: Read entity schema from code definitions
40
34
  const db = this.dbProvider.db;
41
35
  const dialect = getDialect(db.adapter as SQLDatabaseAdapter);
42
- const pgSchema = dialect === 'postgres' ? (getAppConfig().PG_SCHEMA ?? 'public') : 'public';
43
36
 
44
- this.logger.info('Reading entity definitions...');
37
+ // Reset reads entities in the active dialect's canonical form. The generated migration uses
38
+ // dialect-portable builder calls, but cross-dialect parity isn't perfect — see docs.
39
+ this.logger.info(`Reading entity definitions (dialect: ${dialect})...`);
45
40
  const entitySchema = readEntitiesSchema(db, dialect);
46
41
  const tables = Array.from(entitySchema.values());
47
42
  this.logger.info(`Found ${tables.length} entity table(s)`);
48
43
 
49
- // Step 4: Generate DDL by treating all entity tables as "added"
50
- const diff: SchemaDiff = {
51
- dialect,
52
- pgSchema: dialect === 'postgres' ? pgSchema : undefined,
53
- addedTables: tables,
54
- removedTables: [],
55
- modifiedTables: []
56
- };
57
- const statements = generateDDL(diff);
58
-
59
- if (statements.length === 0) {
44
+ if (tables.length === 0) {
60
45
  this.logger.info('No tables found to generate base migration.');
61
46
  return;
62
47
  }
63
48
 
64
- // Step 6: Write migration file
65
- const migrationContent = buildFileContent(statements);
49
+ const migrationContent = generateBuilderMigrationFile(tables);
66
50
  const migrationPath = path.join(this.migrationsDir, '00000000_000000_base.ts');
67
51
 
68
52
  writeFileSync(migrationPath, migrationContent, 'utf8');
@@ -110,6 +110,8 @@ export class MigrationRunCommand {
110
110
 
111
111
  try {
112
112
  await migrationModule.default(this.dbProvider.db);
113
+ // Apply deferred schema-builder statements (FKs) and reset per-migration state.
114
+ await this.dbProvider.db.schema.flush();
113
115
  } catch (err) {
114
116
  this.logger.error('Migration function failed to execute', err, { file });
115
117
  throw err;
@@ -5,11 +5,12 @@ import { getAppConfig } from '../../../app/resolver';
5
5
  import { DBProvider } from '../../../app/state';
6
6
  import { createLogger, pinoLogger } from '../../../services';
7
7
  import { getDialect } from '../../dialect';
8
+ import { generateBuilderMigrationFromDiff } from './builder-regenerator';
8
9
  import { compareSchemas } from './comparator';
9
10
  import { readAllTableNames, readDatabaseSchema } from './db-reader';
10
11
  import { generateDDL } from './ddl-generator';
11
12
  import { readEntitiesSchema } from './entity-reader';
12
- import { generateMigrationFile } from './file-generator';
13
+ import { generateMigrationFile, writeMigrationFile } from './file-generator';
13
14
  import { promptMigrationDescription, setNonInteractive } from './prompt';
14
15
  import { INTERNAL_TABLES } from './schema-model';
15
16
 
@@ -19,7 +20,10 @@ export class MigrationCreateCommand {
19
20
 
20
21
  constructor(private dbProvider: DBProvider) {}
21
22
 
22
- async execute(nonInteractive: boolean & Flag<{ description: 'Skip interactive prompts' }> = false) {
23
+ async execute(
24
+ nonInteractive: boolean & Flag<{ description: 'Skip interactive prompts' }> = false,
25
+ raw: boolean & Flag<{ description: 'Emit raw dialect-specific SQL instead of dialect-portable schema-builder calls' }> = false
26
+ ) {
23
27
  if (nonInteractive) {
24
28
  setNonInteractive(true);
25
29
  }
@@ -50,10 +54,28 @@ export class MigrationCreateCommand {
50
54
  this.logger.info('Comparing schemas...');
51
55
  const diff = await compareSchemas(entitySchema, dbSchema, dialect, !nonInteractive, pgSchema);
52
56
 
53
- // Step 5: Generate DDL
54
- const statements = generateDDL(diff);
57
+ // Step 5: Decide if there are any changes
58
+ const hasChanges =
59
+ diff.addedTables.length > 0 ||
60
+ diff.removedTables.length > 0 ||
61
+ diff.modifiedTables.some(
62
+ t =>
63
+ t.addedColumns.length > 0 ||
64
+ t.removedColumns.length > 0 ||
65
+ t.modifiedColumns.length > 0 ||
66
+ t.renamedColumns.length > 0 ||
67
+ t.addedIndexes.length > 0 ||
68
+ t.removedIndexes.length > 0 ||
69
+ t.renamedIndexes.length > 0 ||
70
+ t.addedForeignKeys.length > 0 ||
71
+ t.removedForeignKeys.length > 0 ||
72
+ t.primaryKeyChanged ||
73
+ t.addedEnumTypes.length > 0 ||
74
+ t.removedEnumTypes.length > 0 ||
75
+ t.modifiedEnumTypes.length > 0
76
+ );
55
77
 
56
- if (statements.length === 0) {
78
+ if (!hasChanges) {
57
79
  this.logger.info('No schema changes detected.');
58
80
  return;
59
81
  }
@@ -74,20 +96,28 @@ export class MigrationCreateCommand {
74
96
  if (table.renamedColumns.length > 0) changes.push(`${table.renamedColumns.length} renamed`);
75
97
  if (table.addedIndexes.length > 0) changes.push(`+${table.addedIndexes.length} idx`);
76
98
  if (table.removedIndexes.length > 0) changes.push(`-${table.removedIndexes.length} idx`);
99
+ if (table.renamedIndexes.length > 0) changes.push(`~${table.renamedIndexes.length} idx name`);
77
100
  if (table.primaryKeyChanged) changes.push('PK changed');
78
101
  this.logger.info(` ${table.tableName}: ${changes.join(', ')}`);
79
102
  }
80
103
 
81
- this.logger.info(`\nDDL statements (${statements.length}):`);
82
- for (const stmt of statements) {
83
- this.logger.info(` ${stmt}`);
84
- }
85
-
86
- // Step 7: Prompt for description and generate file
104
+ // Step 7: Render migration content (builder by default; --raw for legacy SQL)
105
+ let filePath: string;
87
106
  pinoLogger.flush();
88
107
  await new Promise(resolve => setTimeout(resolve, 100));
89
108
  const description = await promptMigrationDescription();
90
- const filePath = generateMigrationFile(statements, description);
109
+
110
+ if (raw) {
111
+ const statements = generateDDL(diff);
112
+ this.logger.info(`\nDDL statements (${statements.length}):`);
113
+ for (const stmt of statements) this.logger.info(` ${stmt}`);
114
+ filePath = generateMigrationFile(statements, description);
115
+ } else {
116
+ const content = generateBuilderMigrationFromDiff(diff);
117
+ this.logger.info(`\nGenerated builder migration (${content.split('\n').length} lines)`);
118
+ filePath = writeMigrationFile(content, description);
119
+ }
120
+
91
121
  this.logger.info(`\nMigration file created: ${filePath}`);
92
122
  } finally {
93
123
  setNonInteractive(false);
@@ -0,0 +1,294 @@
1
+ import { ColumnSchema, Dialect, ForeignKeySchema, IndexSchema, SchemaDiff, TableDiff, TableSchema } from './schema-model';
2
+
3
+ const FILE_HEADER = `import { createMigration } from '@zyno-io/dk-server-foundation';\n\nexport default createMigration(async db => {\n`;
4
+ const FILE_FOOTER = `\n});\n`;
5
+
6
+ /** Render a TypeScript migration file that uses db.schema builder to recreate the given tables. */
7
+ export function generateBuilderMigrationFile(tables: TableSchema[]): string {
8
+ const blocks = tables.map(t => renderTableBlock(t));
9
+ return `${FILE_HEADER}${blocks.join('\n\n')}${FILE_FOOTER}`;
10
+ }
11
+
12
+ /**
13
+ * Render a TypeScript migration file from a SchemaDiff. Used by migration:create to emit
14
+ * portable builder-based migrations.
15
+ *
16
+ * Order: drop removed tables → create added tables → alter modified tables. Within an alter:
17
+ * Schema.alter() internally orders operations dependency-safe, so emission order doesn't matter.
18
+ */
19
+ export function generateBuilderMigrationFromDiff(diff: SchemaDiff): string {
20
+ const blocks: string[] = [];
21
+
22
+ for (const table of diff.removedTables) {
23
+ blocks.push(` await db.schema.drop(${quoteStr(table.name)});`);
24
+ }
25
+
26
+ for (const table of diff.addedTables) {
27
+ blocks.push(renderTableBlock(table));
28
+ }
29
+
30
+ for (const tableDiff of diff.modifiedTables) {
31
+ const block = renderAlterBlock(tableDiff, diff.dialect);
32
+ if (block) blocks.push(block);
33
+ }
34
+
35
+ if (blocks.length === 0) {
36
+ return `${FILE_HEADER} // No schema changes detected.${FILE_FOOTER}`;
37
+ }
38
+ return `${FILE_HEADER}${blocks.join('\n\n')}${FILE_FOOTER}`;
39
+ }
40
+
41
+ function renderAlterBlock(td: TableDiff, dialect: Dialect): string {
42
+ const preLines: string[] = [];
43
+ const innerLines: string[] = [];
44
+ const postLines: string[] = [];
45
+
46
+ // PG enum lifecycle (raw SQL — first-class builder support is post-Phase-2)
47
+ if (dialect === 'postgres') {
48
+ for (const newType of td.addedEnumTypes) {
49
+ preLines.push(` await db.schema.enumType(${quoteStr(newType.typeName)}, [${newType.values.map(quoteStr).join(', ')}]);`);
50
+ }
51
+ for (const mod of td.modifiedEnumTypes) {
52
+ for (const value of mod.added) {
53
+ preLines.push(
54
+ ` await db.schema.raw(${quoteStr(`ALTER TYPE "${mod.typeName}" ADD VALUE IF NOT EXISTS '${value.replace(/'/g, "''")}'`)});`
55
+ );
56
+ }
57
+ }
58
+ for (const typeName of td.removedEnumTypes) {
59
+ postLines.push(` await db.schema.raw(${quoteStr(`DROP CAST IF EXISTS (text AS "${typeName}")`)});`);
60
+ postLines.push(` await db.schema.raw(${quoteStr(`DROP TYPE IF EXISTS "${typeName}"`)});`);
61
+ }
62
+ }
63
+
64
+ // PK changes (drop existing first, replacement comes via .primary())
65
+ if (td.primaryKeyChanged && td.oldPrimaryKey && td.oldPrimaryKey.length > 0) {
66
+ innerLines.push(` t.dropPrimary();`);
67
+ }
68
+
69
+ for (const fk of td.removedForeignKeys) innerLines.push(` t.dropForeign(${quoteStr(fk.name)});`);
70
+ for (const idx of td.removedIndexes) innerLines.push(` t.dropIndex(${quoteStr(idx.name)});`);
71
+ for (const r of td.renamedIndexes) innerLines.push(` t.renameIndex(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
72
+ for (const col of td.removedColumns) innerLines.push(` t.dropColumn(${quoteStr(col.name)});`);
73
+ for (const r of td.renamedColumns) innerLines.push(` t.renameColumn(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
74
+
75
+ const stubTable: TableSchema = { name: td.tableName, columns: [], indexes: [], foreignKeys: [] };
76
+
77
+ for (const col of td.addedColumns) {
78
+ let line = renderColumnLine(col, stubTable);
79
+ // MySQL: if we know the entity-order position, emit .after() / .first() to preserve column layout
80
+ if (dialect === 'mysql' && td.entityColumns) {
81
+ const positioning = renderPositioning(col, td.entityColumns, td.addedColumns);
82
+ if (positioning) line = line.replace(/;$/, `${positioning};`);
83
+ }
84
+ innerLines.push(` ${line}`);
85
+ }
86
+ for (const mod of td.modifiedColumns) {
87
+ // Modified column: render as if adding, append .change()
88
+ const line = renderColumnLine(mod.newColumn, stubTable);
89
+ innerLines.push(` ${line.replace(/;$/, '.change();')}`);
90
+ }
91
+
92
+ if (td.primaryKeyChanged && td.newPrimaryKey && td.newPrimaryKey.length > 0) {
93
+ innerLines.push(` t.primary([${td.newPrimaryKey.map(quoteStr).join(', ')}]);`);
94
+ }
95
+
96
+ for (const idx of td.addedIndexes) innerLines.push(` ${renderIndexLine(idx, stubTable)}`);
97
+ for (const fk of td.addedForeignKeys) innerLines.push(` ${renderForeignKeyLine(fk, stubTable)}`);
98
+
99
+ if (innerLines.length === 0 && preLines.length === 0 && postLines.length === 0) return '';
100
+
101
+ const parts: string[] = [];
102
+ if (preLines.length > 0) parts.push(preLines.join('\n'));
103
+ if (innerLines.length > 0) {
104
+ parts.push(` await db.schema.alter(${quoteStr(td.tableName)}, t => {\n${innerLines.join('\n')}\n });`);
105
+ }
106
+ if (postLines.length > 0) parts.push(postLines.join('\n'));
107
+ return parts.join('\n\n');
108
+ }
109
+
110
+ function renderTableBlock(table: TableSchema): string {
111
+ const lines: string[] = [];
112
+
113
+ for (const col of table.columns) {
114
+ lines.push(` ${renderColumnLine(col, table)}`);
115
+ }
116
+
117
+ // Composite PK — single-column PK is emitted via .primary() on the column line
118
+ const pkCols = table.columns.filter(c => c.isPrimaryKey).map(c => c.name);
119
+ if (pkCols.length > 1) {
120
+ lines.push(` t.primary([${pkCols.map(quoteStr).join(', ')}]);`);
121
+ }
122
+
123
+ for (const idx of table.indexes) {
124
+ lines.push(` ${renderIndexLine(idx, table)}`);
125
+ }
126
+
127
+ for (const fk of table.foreignKeys) {
128
+ lines.push(` ${renderForeignKeyLine(fk, table)}`);
129
+ }
130
+
131
+ return ` await db.schema.create(${quoteStr(table.name)}, t => {\n${lines.join('\n')}\n });`;
132
+ }
133
+
134
+ /** Emit `.after('x')` / `.first()` based on entity-order position of an added column. */
135
+ function renderPositioning(col: ColumnSchema, entityColumns: ColumnSchema[], addedColumns: ColumnSchema[]): string {
136
+ const idx = entityColumns.findIndex(c => c.name === col.name);
137
+ if (idx < 0) return '';
138
+ const addedNames = new Set(addedColumns.map(c => c.name));
139
+ // Walk backwards to find the previous column that already exists in the DB (not in the same add batch)
140
+ for (let i = idx - 1; i >= 0; i--) {
141
+ const prev = entityColumns[i];
142
+ if (!addedNames.has(prev.name)) return `.after(${quoteStr(prev.name)})`;
143
+ }
144
+ return '.first()';
145
+ }
146
+
147
+ function renderColumnLine(col: ColumnSchema, table: TableSchema): string {
148
+ let line = `t.${pickBuilderMethod(col)}`;
149
+
150
+ // Modifier order chosen to read naturally
151
+ if (col.unsigned) line += '.unsigned()';
152
+ if (col.nullable) line += '.nullable()';
153
+ if (col.autoIncrement) line += '.autoIncrement()';
154
+
155
+ const pkCount = table.columns.filter(c => c.isPrimaryKey).length;
156
+ if (col.isPrimaryKey && pkCount === 1) line += '.primary()';
157
+
158
+ if (col.defaultExpression) {
159
+ line += `.defaultRaw(${quoteStr(col.defaultExpression)})`;
160
+ } else if (col.defaultValue !== undefined) {
161
+ line += `.default(${renderJsValue(col.defaultValue)})`;
162
+ }
163
+
164
+ if (col.onUpdateExpression) {
165
+ line += `.onUpdate(${quoteStr(col.onUpdateExpression)})`;
166
+ }
167
+
168
+ return line + ';';
169
+ }
170
+
171
+ function pickBuilderMethod(col: ColumnSchema): string {
172
+ const n = quoteStr(col.name);
173
+ switch (col.type) {
174
+ case 'varchar':
175
+ return `string(${n}, ${col.size ?? 255})`;
176
+ case 'char':
177
+ // CHAR(36) is the canonical string-form UUID storage (dksf:uuid annotation)
178
+ if (col.size === 36) return `uuidString(${n})`;
179
+ return `char(${n}, ${col.size ?? 1})`;
180
+ case 'text':
181
+ return `text(${n})`;
182
+ case 'tinytext':
183
+ return `tinyText(${n})`;
184
+ case 'mediumtext':
185
+ return `mediumText(${n})`;
186
+ case 'longtext':
187
+ return `longText(${n})`;
188
+ case 'tinyint':
189
+ // tinyint(1) is the canonical MySQL boolean storage; map back to .boolean()
190
+ if (col.size === 1) return `boolean(${n})`;
191
+ return `tinyint(${n})`;
192
+ case 'smallint':
193
+ return `smallint(${n})`;
194
+ case 'int':
195
+ case 'integer':
196
+ return `integer(${n})`;
197
+ case 'bigint':
198
+ return `bigInteger(${n})`;
199
+ case 'boolean':
200
+ return `boolean(${n})`;
201
+ case 'float':
202
+ case 'real':
203
+ return `float(${n})`;
204
+ case 'double':
205
+ case 'double precision':
206
+ return `double(${n})`;
207
+ case 'decimal':
208
+ case 'numeric': {
209
+ const args = [n];
210
+ if (col.size !== undefined) {
211
+ args.push(String(col.size));
212
+ if (col.scale !== undefined) args.push(String(col.scale));
213
+ }
214
+ return `decimal(${args.join(', ')})`;
215
+ }
216
+ case 'date':
217
+ return `date(${n})`;
218
+ case 'datetime':
219
+ case 'timestamp':
220
+ return `dateTime(${n})`;
221
+ case 'timestamptz':
222
+ return `timestamptz(${n})`;
223
+ case 'binary':
224
+ // BINARY(16) is the canonical UUID storage on MySQL (Deepkit's UUIDv4 annotation)
225
+ if (col.size === 16) return `uuid(${n})`;
226
+ return `binary(${n}, ${col.size ?? 16})`;
227
+ case 'blob':
228
+ case 'bytea':
229
+ return `blob(${n})`;
230
+ case 'json':
231
+ return `json(${n})`;
232
+ case 'jsonb':
233
+ return `jsonb(${n})`;
234
+ case 'uuid':
235
+ return `uuid(${n})`;
236
+ case 'point':
237
+ return `point(${n})`;
238
+ case 'enum': {
239
+ const values = col.enumValues ?? [];
240
+ const valuesArg = `[${values.map(v => quoteStr(v)).join(', ')}]`;
241
+ const typeName = col.enumTypeName;
242
+ const args = typeName ? [n, valuesArg, quoteStr(typeName)] : [n, valuesArg];
243
+ return `enum(${args.join(', ')})`;
244
+ }
245
+ default:
246
+ throw new Error(`Unsupported column type for builder regeneration: '${col.type}' (column ${col.name})`);
247
+ }
248
+ }
249
+
250
+ function renderIndexLine(idx: IndexSchema, table: TableSchema): string {
251
+ const colsArg = renderColumnsArg(idx.columns);
252
+
253
+ const suffix = idx.spatial ? 'spatial' : idx.unique ? 'unique' : 'index';
254
+ const expectedName = `${table.name}_${idx.columns.join('_')}_${suffix}`;
255
+ const nameArg = idx.name === expectedName ? '' : `, ${quoteStr(idx.name)}`;
256
+
257
+ if (idx.spatial) return `t.spatialIndex(${colsArg}${nameArg});`;
258
+ if (idx.unique) return `t.unique(${colsArg}${nameArg});`;
259
+ return `t.index(${colsArg}${nameArg});`;
260
+ }
261
+
262
+ function renderForeignKeyLine(fk: ForeignKeySchema, table: TableSchema): string {
263
+ const colsArg = renderColumnsArg(fk.columns);
264
+ const expectedName = `${table.name}_${fk.columns.join('_')}_foreign`;
265
+ const nameArg = fk.name === expectedName ? '' : `, ${quoteStr(fk.name)}`;
266
+
267
+ let line = `t.foreign(${colsArg}${nameArg})`;
268
+ if (fk.referencedColumns.length === 1) {
269
+ line += `.references(${quoteStr(fk.referencedColumns[0])})`;
270
+ } else {
271
+ line += `.referencesAll([${fk.referencedColumns.map(quoteStr).join(', ')}])`;
272
+ }
273
+ line += `.on(${quoteStr(fk.referencedTable)})`;
274
+ if (fk.onDelete && fk.onDelete.toUpperCase() !== 'RESTRICT') line += `.onDelete(${quoteStr(fk.onDelete)})`;
275
+ if (fk.onUpdate && fk.onUpdate.toUpperCase() !== 'RESTRICT') line += `.onUpdate(${quoteStr(fk.onUpdate)})`;
276
+ return line + ';';
277
+ }
278
+
279
+ function renderColumnsArg(columns: string[]): string {
280
+ if (columns.length === 1) return quoteStr(columns[0]);
281
+ return `[${columns.map(quoteStr).join(', ')}]`;
282
+ }
283
+
284
+ function quoteStr(s: string): string {
285
+ return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
286
+ }
287
+
288
+ function renderJsValue(value: unknown): string {
289
+ if (value === null) return 'null';
290
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
291
+ if (typeof value === 'number') return String(value);
292
+ if (typeof value === 'string') return quoteStr(value);
293
+ return JSON.stringify(value);
294
+ }
@@ -73,6 +73,7 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
73
73
  reorderedColumns: [],
74
74
  addedIndexes: [],
75
75
  removedIndexes: [],
76
+ renamedIndexes: [],
76
77
  addedForeignKeys: [],
77
78
  removedForeignKeys: [],
78
79
  primaryKeyChanged: false,
@@ -154,9 +155,10 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
154
155
  }
155
156
 
156
157
  // --- Indexes ---
157
- const { added: addedIdx, removed: removedIdx } = compareIndexes(entityTable.indexes, dbTable.indexes);
158
+ const { added: addedIdx, removed: removedIdx, renamed: renamedIdx } = compareIndexes(entityTable.indexes, dbTable.indexes);
158
159
  diff.addedIndexes = addedIdx;
159
160
  diff.removedIndexes = removedIdx;
161
+ diff.renamedIndexes = renamedIdx;
160
162
 
161
163
  // --- Foreign Keys ---
162
164
  const { added: addedFK, removed: removedFK } = compareForeignKeys(entityTable.foreignKeys, dbTable.foreignKeys);
@@ -177,6 +179,7 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
177
179
  diff.reorderedColumns.length === 0 &&
178
180
  diff.addedIndexes.length === 0 &&
179
181
  diff.removedIndexes.length === 0 &&
182
+ diff.renamedIndexes.length === 0 &&
180
183
  diff.addedForeignKeys.length === 0 &&
181
184
  diff.removedForeignKeys.length === 0 &&
182
185
  !diff.primaryKeyChanged &&
@@ -288,7 +291,16 @@ function defaultsMatch(a: ColumnSchema, b: ColumnSchema): boolean {
288
291
  if (a.defaultValue === undefined && b.defaultValue === undefined) return true;
289
292
  if (a.defaultValue === undefined || b.defaultValue === undefined) return false;
290
293
 
291
- return String(a.defaultValue) === String(b.defaultValue);
294
+ const av = String(a.defaultValue);
295
+ const bv = String(b.defaultValue);
296
+ if (av === bv) return true;
297
+
298
+ // Numeric defaults: '1' and '1.0'/'1.00' are equivalent (MySQL reports decimal defaults with scale).
299
+ const an = Number(av);
300
+ const bn = Number(bv);
301
+ if (av.trim() !== '' && bv.trim() !== '' && !Number.isNaN(an) && !Number.isNaN(bn)) return an === bn;
302
+
303
+ return false;
292
304
  }
293
305
 
294
306
  async function detectRenames(
@@ -356,7 +368,10 @@ function detectReorderingMySQL(entityTable: TableSchema, dbTable: TableSchema, d
356
368
  return reorders;
357
369
  }
358
370
 
359
- function compareIndexes(entityIndexes: IndexSchema[], dbIndexes: IndexSchema[]): { added: IndexSchema[]; removed: IndexSchema[] } {
371
+ function compareIndexes(
372
+ entityIndexes: IndexSchema[],
373
+ dbIndexes: IndexSchema[]
374
+ ): { added: IndexSchema[]; removed: IndexSchema[]; renamed: { from: string; to: string }[] } {
360
375
  // Match by column set + uniqueness + spatial, not by name
361
376
  const indexKey = (idx: IndexSchema) => `${idx.columns.join(',')}:${idx.unique}:${idx.spatial}`;
362
377
 
@@ -366,7 +381,22 @@ function compareIndexes(entityIndexes: IndexSchema[], dbIndexes: IndexSchema[]):
366
381
  const added = entityIndexes.filter(i => !dbKeys.has(indexKey(i)));
367
382
  const removed = dbIndexes.filter(i => !entityKeys.has(indexKey(i)));
368
383
 
369
- return { added, removed };
384
+ // Name reconciliation: when an entity index sets an explicit name but the column-matched DB
385
+ // index carries a different name, rename it (instead of leaving the index permanently under a
386
+ // stale/auto name). Skip when the target name is still held by a DB index we're keeping — that
387
+ // would collide; the rename only fires once the conflicting index is among `removed`.
388
+ const removedNames = new Set(removed.map(i => i.name));
389
+ const keptDbNames = new Set(dbIndexes.filter(i => !removedNames.has(i.name)).map(i => i.name));
390
+ const renamed: { from: string; to: string }[] = [];
391
+ for (const e of entityIndexes) {
392
+ if (!e.explicitName) continue;
393
+ const dbMatch = dbKeys.get(indexKey(e));
394
+ if (!dbMatch || dbMatch.name === e.name) continue;
395
+ if (keptDbNames.has(e.name)) continue; // target name taken by an index we're keeping
396
+ renamed.push({ from: dbMatch.name, to: e.name });
397
+ }
398
+
399
+ return { added, removed, renamed };
370
400
  }
371
401
 
372
402
  function normalizeFkAction(action: string): string {