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

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 (113) 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 +33 -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 +300 -0
  31. package/dist/src/database/migration/create/builder-regenerator.js.map +1 -0
  32. package/dist/src/database/migration/create/db-reader.js +47 -6
  33. package/dist/src/database/migration/create/db-reader.js.map +1 -1
  34. package/dist/src/database/migration/create/ddl-generator.d.ts.map +1 -1
  35. package/dist/src/database/migration/create/ddl-generator.js +36 -206
  36. package/dist/src/database/migration/create/ddl-generator.js.map +1 -1
  37. package/dist/src/database/migration/create/file-generator.d.ts +2 -0
  38. package/dist/src/database/migration/create/file-generator.d.ts.map +1 -1
  39. package/dist/src/database/migration/create/file-generator.js +7 -2
  40. package/dist/src/database/migration/create/file-generator.js.map +1 -1
  41. package/dist/src/database/migration/create/schema-model.d.ts +2 -0
  42. package/dist/src/database/migration/create/schema-model.d.ts.map +1 -1
  43. package/dist/src/database/migration/create/schema-model.js +1 -1
  44. package/dist/src/database/mysql.d.ts.map +1 -1
  45. package/dist/src/database/mysql.js +5 -11
  46. package/dist/src/database/mysql.js.map +1 -1
  47. package/dist/src/database/postgres.d.ts +1 -0
  48. package/dist/src/database/postgres.d.ts.map +1 -1
  49. package/dist/src/database/postgres.js +1 -13
  50. package/dist/src/database/postgres.js.map +1 -1
  51. package/dist/src/database/schema/AlterBlueprint.d.ts +44 -0
  52. package/dist/src/database/schema/AlterBlueprint.d.ts.map +1 -0
  53. package/dist/src/database/schema/AlterBlueprint.js +82 -0
  54. package/dist/src/database/schema/AlterBlueprint.js.map +1 -0
  55. package/dist/src/database/schema/Blueprint.d.ts +16 -0
  56. package/dist/src/database/schema/Blueprint.d.ts.map +1 -0
  57. package/dist/src/database/schema/Blueprint.js +51 -0
  58. package/dist/src/database/schema/Blueprint.js.map +1 -0
  59. package/dist/src/database/schema/BlueprintBase.d.ts +59 -0
  60. package/dist/src/database/schema/BlueprintBase.d.ts.map +1 -0
  61. package/dist/src/database/schema/BlueprintBase.js +349 -0
  62. package/dist/src/database/schema/BlueprintBase.js.map +1 -0
  63. package/dist/src/database/schema/ColumnDefinition.d.ts +43 -0
  64. package/dist/src/database/schema/ColumnDefinition.d.ts.map +1 -0
  65. package/dist/src/database/schema/ColumnDefinition.js +117 -0
  66. package/dist/src/database/schema/ColumnDefinition.js.map +1 -0
  67. package/dist/src/database/schema/Schema.d.ts +32 -0
  68. package/dist/src/database/schema/Schema.d.ts.map +1 -0
  69. package/dist/src/database/schema/Schema.js +148 -0
  70. package/dist/src/database/schema/Schema.js.map +1 -0
  71. package/dist/src/database/schema/grammar/Grammar.d.ts +34 -0
  72. package/dist/src/database/schema/grammar/Grammar.d.ts.map +1 -0
  73. package/dist/src/database/schema/grammar/Grammar.js +36 -0
  74. package/dist/src/database/schema/grammar/Grammar.js.map +1 -0
  75. package/dist/src/database/schema/grammar/MySQLGrammar.d.ts +28 -0
  76. package/dist/src/database/schema/grammar/MySQLGrammar.d.ts.map +1 -0
  77. package/dist/src/database/schema/grammar/MySQLGrammar.js +190 -0
  78. package/dist/src/database/schema/grammar/MySQLGrammar.js.map +1 -0
  79. package/dist/src/database/schema/grammar/PostgresGrammar.d.ts +28 -0
  80. package/dist/src/database/schema/grammar/PostgresGrammar.d.ts.map +1 -0
  81. package/dist/src/database/schema/grammar/PostgresGrammar.js +223 -0
  82. package/dist/src/database/schema/grammar/PostgresGrammar.js.map +1 -0
  83. package/dist/src/database/schema/index.d.ts +9 -0
  84. package/dist/src/database/schema/index.d.ts.map +1 -0
  85. package/dist/src/database/schema/index.js +12 -0
  86. package/dist/src/database/schema/index.js.map +1 -0
  87. package/docs/content/cli.md +4 -1
  88. package/docs/content/configuration.md +6 -0
  89. package/docs/content/database.md +128 -0
  90. package/package.json +1 -1
  91. package/src/app/config.ts +2 -0
  92. package/src/database/common.ts +51 -0
  93. package/src/database/factory.ts +59 -0
  94. package/src/database/index.ts +2 -0
  95. package/src/database/migration/MigrationResetCommand.ts +6 -22
  96. package/src/database/migration/MigrationRunCommand.ts +2 -0
  97. package/src/database/migration/create/MigrationCreateCommand.ts +40 -12
  98. package/src/database/migration/create/builder-regenerator.ts +287 -0
  99. package/src/database/migration/create/db-reader.ts +43 -6
  100. package/src/database/migration/create/ddl-generator.ts +35 -212
  101. package/src/database/migration/create/file-generator.ts +5 -1
  102. package/src/database/migration/create/schema-model.ts +2 -0
  103. package/src/database/mysql.ts +7 -15
  104. package/src/database/postgres.ts +3 -17
  105. package/src/database/schema/AlterBlueprint.ts +85 -0
  106. package/src/database/schema/Blueprint.ts +43 -0
  107. package/src/database/schema/BlueprintBase.ts +386 -0
  108. package/src/database/schema/ColumnDefinition.ts +131 -0
  109. package/src/database/schema/Schema.ts +164 -0
  110. package/src/database/schema/grammar/Grammar.ts +61 -0
  111. package/src/database/schema/grammar/MySQLGrammar.ts +196 -0
  112. package/src/database/schema/grammar/PostgresGrammar.ts +232 -0
  113. package/src/database/schema/index.ts +8 -0
@@ -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,27 @@ 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.addedForeignKeys.length > 0 ||
70
+ t.removedForeignKeys.length > 0 ||
71
+ t.primaryKeyChanged ||
72
+ t.addedEnumTypes.length > 0 ||
73
+ t.removedEnumTypes.length > 0 ||
74
+ t.modifiedEnumTypes.length > 0
75
+ );
55
76
 
56
- if (statements.length === 0) {
77
+ if (!hasChanges) {
57
78
  this.logger.info('No schema changes detected.');
58
79
  return;
59
80
  }
@@ -78,16 +99,23 @@ export class MigrationCreateCommand {
78
99
  this.logger.info(` ${table.tableName}: ${changes.join(', ')}`);
79
100
  }
80
101
 
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
102
+ // Step 7: Render migration content (builder by default; --raw for legacy SQL)
103
+ let filePath: string;
87
104
  pinoLogger.flush();
88
105
  await new Promise(resolve => setTimeout(resolve, 100));
89
106
  const description = await promptMigrationDescription();
90
- const filePath = generateMigrationFile(statements, description);
107
+
108
+ if (raw) {
109
+ const statements = generateDDL(diff);
110
+ this.logger.info(`\nDDL statements (${statements.length}):`);
111
+ for (const stmt of statements) this.logger.info(` ${stmt}`);
112
+ filePath = generateMigrationFile(statements, description);
113
+ } else {
114
+ const content = generateBuilderMigrationFromDiff(diff);
115
+ this.logger.info(`\nGenerated builder migration (${content.split('\n').length} lines)`);
116
+ filePath = writeMigrationFile(content, description);
117
+ }
118
+
91
119
  this.logger.info(`\nMigration file created: ${filePath}`);
92
120
  } finally {
93
121
  setNonInteractive(false);
@@ -0,0 +1,287 @@
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 col of td.removedColumns) innerLines.push(` t.dropColumn(${quoteStr(col.name)});`);
72
+ for (const r of td.renamedColumns) innerLines.push(` t.renameColumn(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
73
+
74
+ const stubTable: TableSchema = { name: td.tableName, columns: [], indexes: [], foreignKeys: [] };
75
+
76
+ for (const col of td.addedColumns) {
77
+ let line = renderColumnLine(col, stubTable);
78
+ // MySQL: if we know the entity-order position, emit .after() / .first() to preserve column layout
79
+ if (dialect === 'mysql' && td.entityColumns) {
80
+ const positioning = renderPositioning(col, td.entityColumns, td.addedColumns);
81
+ if (positioning) line = line.replace(/;$/, `${positioning};`);
82
+ }
83
+ innerLines.push(` ${line}`);
84
+ }
85
+ for (const mod of td.modifiedColumns) {
86
+ // Modified column: render as if adding, append .change()
87
+ const line = renderColumnLine(mod.newColumn, stubTable);
88
+ innerLines.push(` ${line.replace(/;$/, '.change();')}`);
89
+ }
90
+
91
+ if (td.primaryKeyChanged && td.newPrimaryKey && td.newPrimaryKey.length > 0) {
92
+ innerLines.push(` t.primary([${td.newPrimaryKey.map(quoteStr).join(', ')}]);`);
93
+ }
94
+
95
+ for (const idx of td.addedIndexes) innerLines.push(` ${renderIndexLine(idx, stubTable)}`);
96
+ for (const fk of td.addedForeignKeys) innerLines.push(` ${renderForeignKeyLine(fk, stubTable)}`);
97
+
98
+ if (innerLines.length === 0 && preLines.length === 0 && postLines.length === 0) return '';
99
+
100
+ const parts: string[] = [];
101
+ if (preLines.length > 0) parts.push(preLines.join('\n'));
102
+ if (innerLines.length > 0) {
103
+ parts.push(` await db.schema.alter(${quoteStr(td.tableName)}, t => {\n${innerLines.join('\n')}\n });`);
104
+ }
105
+ if (postLines.length > 0) parts.push(postLines.join('\n'));
106
+ return parts.join('\n\n');
107
+ }
108
+
109
+ function renderTableBlock(table: TableSchema): string {
110
+ const lines: string[] = [];
111
+
112
+ for (const col of table.columns) {
113
+ lines.push(` ${renderColumnLine(col, table)}`);
114
+ }
115
+
116
+ // Composite PK — single-column PK is emitted via .primary() on the column line
117
+ const pkCols = table.columns.filter(c => c.isPrimaryKey).map(c => c.name);
118
+ if (pkCols.length > 1) {
119
+ lines.push(` t.primary([${pkCols.map(quoteStr).join(', ')}]);`);
120
+ }
121
+
122
+ for (const idx of table.indexes) {
123
+ lines.push(` ${renderIndexLine(idx, table)}`);
124
+ }
125
+
126
+ for (const fk of table.foreignKeys) {
127
+ lines.push(` ${renderForeignKeyLine(fk, table)}`);
128
+ }
129
+
130
+ return ` await db.schema.create(${quoteStr(table.name)}, t => {\n${lines.join('\n')}\n });`;
131
+ }
132
+
133
+ /** Emit `.after('x')` / `.first()` based on entity-order position of an added column. */
134
+ function renderPositioning(col: ColumnSchema, entityColumns: ColumnSchema[], addedColumns: ColumnSchema[]): string {
135
+ const idx = entityColumns.findIndex(c => c.name === col.name);
136
+ if (idx < 0) return '';
137
+ const addedNames = new Set(addedColumns.map(c => c.name));
138
+ // Walk backwards to find the previous column that already exists in the DB (not in the same add batch)
139
+ for (let i = idx - 1; i >= 0; i--) {
140
+ const prev = entityColumns[i];
141
+ if (!addedNames.has(prev.name)) return `.after(${quoteStr(prev.name)})`;
142
+ }
143
+ return '.first()';
144
+ }
145
+
146
+ function renderColumnLine(col: ColumnSchema, table: TableSchema): string {
147
+ let line = `t.${pickBuilderMethod(col)}`;
148
+
149
+ // Modifier order chosen to read naturally
150
+ if (col.unsigned) line += '.unsigned()';
151
+ if (col.nullable) line += '.nullable()';
152
+ if (col.autoIncrement) line += '.autoIncrement()';
153
+
154
+ const pkCount = table.columns.filter(c => c.isPrimaryKey).length;
155
+ if (col.isPrimaryKey && pkCount === 1) line += '.primary()';
156
+
157
+ if (col.defaultExpression) {
158
+ line += `.defaultRaw(${quoteStr(col.defaultExpression)})`;
159
+ } else if (col.defaultValue !== undefined) {
160
+ line += `.default(${renderJsValue(col.defaultValue)})`;
161
+ }
162
+
163
+ if (col.onUpdateExpression) {
164
+ line += `.onUpdate(${quoteStr(col.onUpdateExpression)})`;
165
+ }
166
+
167
+ return line + ';';
168
+ }
169
+
170
+ function pickBuilderMethod(col: ColumnSchema): string {
171
+ const n = quoteStr(col.name);
172
+ switch (col.type) {
173
+ case 'varchar':
174
+ return `string(${n}, ${col.size ?? 255})`;
175
+ case 'char':
176
+ // CHAR(36) is the canonical string-form UUID storage (dksf:uuid annotation)
177
+ if (col.size === 36) return `uuidString(${n})`;
178
+ return `char(${n}, ${col.size ?? 1})`;
179
+ case 'text':
180
+ return `text(${n})`;
181
+ case 'tinyint':
182
+ // tinyint(1) is the canonical MySQL boolean storage; map back to .boolean()
183
+ if (col.size === 1) return `boolean(${n})`;
184
+ return `tinyint(${n})`;
185
+ case 'smallint':
186
+ return `smallint(${n})`;
187
+ case 'int':
188
+ case 'integer':
189
+ return `integer(${n})`;
190
+ case 'bigint':
191
+ return `bigInteger(${n})`;
192
+ case 'boolean':
193
+ return `boolean(${n})`;
194
+ case 'float':
195
+ case 'real':
196
+ return `float(${n})`;
197
+ case 'double':
198
+ case 'double precision':
199
+ return `double(${n})`;
200
+ case 'decimal':
201
+ case 'numeric': {
202
+ const args = [n];
203
+ if (col.size !== undefined) {
204
+ args.push(String(col.size));
205
+ if (col.scale !== undefined) args.push(String(col.scale));
206
+ }
207
+ return `decimal(${args.join(', ')})`;
208
+ }
209
+ case 'date':
210
+ return `date(${n})`;
211
+ case 'datetime':
212
+ case 'timestamp':
213
+ return `dateTime(${n})`;
214
+ case 'timestamptz':
215
+ return `timestamptz(${n})`;
216
+ case 'binary':
217
+ // BINARY(16) is the canonical UUID storage on MySQL (Deepkit's UUIDv4 annotation)
218
+ if (col.size === 16) return `uuid(${n})`;
219
+ return `binary(${n}, ${col.size ?? 16})`;
220
+ case 'blob':
221
+ case 'bytea':
222
+ return `blob(${n})`;
223
+ case 'json':
224
+ return `json(${n})`;
225
+ case 'jsonb':
226
+ return `jsonb(${n})`;
227
+ case 'uuid':
228
+ return `uuid(${n})`;
229
+ case 'point':
230
+ return `point(${n})`;
231
+ case 'enum': {
232
+ const values = col.enumValues ?? [];
233
+ const valuesArg = `[${values.map(v => quoteStr(v)).join(', ')}]`;
234
+ const typeName = col.enumTypeName;
235
+ const args = typeName ? [n, valuesArg, quoteStr(typeName)] : [n, valuesArg];
236
+ return `enum(${args.join(', ')})`;
237
+ }
238
+ default:
239
+ throw new Error(`Unsupported column type for builder regeneration: '${col.type}' (column ${col.name})`);
240
+ }
241
+ }
242
+
243
+ function renderIndexLine(idx: IndexSchema, table: TableSchema): string {
244
+ const colsArg = renderColumnsArg(idx.columns);
245
+
246
+ const suffix = idx.spatial ? 'spatial' : idx.unique ? 'unique' : 'index';
247
+ const expectedName = `${table.name}_${idx.columns.join('_')}_${suffix}`;
248
+ const nameArg = idx.name === expectedName ? '' : `, ${quoteStr(idx.name)}`;
249
+
250
+ if (idx.spatial) return `t.spatialIndex(${colsArg}${nameArg});`;
251
+ if (idx.unique) return `t.unique(${colsArg}${nameArg});`;
252
+ return `t.index(${colsArg}${nameArg});`;
253
+ }
254
+
255
+ function renderForeignKeyLine(fk: ForeignKeySchema, table: TableSchema): string {
256
+ const colsArg = renderColumnsArg(fk.columns);
257
+ const expectedName = `${table.name}_${fk.columns.join('_')}_foreign`;
258
+ const nameArg = fk.name === expectedName ? '' : `, ${quoteStr(fk.name)}`;
259
+
260
+ let line = `t.foreign(${colsArg}${nameArg})`;
261
+ if (fk.referencedColumns.length === 1) {
262
+ line += `.references(${quoteStr(fk.referencedColumns[0])})`;
263
+ } else {
264
+ line += `.referencesAll([${fk.referencedColumns.map(quoteStr).join(', ')}])`;
265
+ }
266
+ line += `.on(${quoteStr(fk.referencedTable)})`;
267
+ if (fk.onDelete && fk.onDelete.toUpperCase() !== 'RESTRICT') line += `.onDelete(${quoteStr(fk.onDelete)})`;
268
+ if (fk.onUpdate && fk.onUpdate.toUpperCase() !== 'RESTRICT') line += `.onUpdate(${quoteStr(fk.onUpdate)})`;
269
+ return line + ';';
270
+ }
271
+
272
+ function renderColumnsArg(columns: string[]): string {
273
+ if (columns.length === 1) return quoteStr(columns[0]);
274
+ return `[${columns.map(quoteStr).join(', ')}]`;
275
+ }
276
+
277
+ function quoteStr(s: string): string {
278
+ return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
279
+ }
280
+
281
+ function renderJsValue(value: unknown): string {
282
+ if (value === null) return 'null';
283
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
284
+ if (typeof value === 'number') return String(value);
285
+ if (typeof value === 'string') return quoteStr(value);
286
+ return JSON.stringify(value);
287
+ }
@@ -47,9 +47,14 @@ async function readMySQLTable(db: BaseDatabase, tableName: string): Promise<Tabl
47
47
  const columns = await readMySQLColumns(db, tableName);
48
48
  if (columns.length === 0) return null;
49
49
 
50
- const indexes = await readMySQLIndexes(db, tableName);
50
+ const allIndexes = await readMySQLIndexes(db, tableName);
51
51
  const foreignKeys = await readMySQLForeignKeys(db, tableName);
52
52
 
53
+ // MySQL auto-creates an index for FK columns (same name as the FK) if no covering index exists.
54
+ // Filter those out so they don't show up as spurious added/removed-index diffs.
55
+ const fkNames = new Set(foreignKeys.map(fk => fk.name));
56
+ const indexes = allIndexes.filter(idx => !fkNames.has(idx.name));
57
+
53
58
  return { name: tableName, columns, indexes, foreignKeys };
54
59
  }
55
60
 
@@ -74,7 +79,9 @@ async function readMySQLColumns(db: BaseDatabase, tableName: string): Promise<Co
74
79
  name: row.COLUMN_NAME,
75
80
  type: normalizeMySQLType(dataType, columnType),
76
81
  size: inferMySQLSize(dataType, columnType, row.CHARACTER_MAXIMUM_LENGTH, row.NUMERIC_PRECISION),
77
- scale: row.NUMERIC_SCALE != null ? Number(row.NUMERIC_SCALE) : undefined,
82
+ // Scale is only meaningful for decimal/numeric. MySQL reports NUMERIC_SCALE=0
83
+ // for ints, which would cause spurious typeChanged diffs against entity-reader output.
84
+ scale: (dataType === 'decimal' || dataType === 'numeric') && row.NUMERIC_SCALE != null ? Number(row.NUMERIC_SCALE) : undefined,
78
85
  unsigned: columnType.includes('unsigned'),
79
86
  nullable: row.IS_NULLABLE === 'YES',
80
87
  autoIncrement: extra.includes('auto_increment'),
@@ -230,7 +237,7 @@ async function readPostgresColumns(db: BaseDatabase, tableName: string, pgSchema
230
237
  name: row.column_name,
231
238
  type: dataType,
232
239
  size: inferPostgresSize(dataType, row.character_maximum_length, row.numeric_precision),
233
- scale: row.numeric_scale != null ? Number(row.numeric_scale) : undefined,
240
+ scale: (dataType === 'decimal' || dataType === 'numeric') && row.numeric_scale != null ? Number(row.numeric_scale) : undefined,
234
241
  unsigned: false,
235
242
  nullable: row.is_nullable === 'YES',
236
243
  autoIncrement: isSerial,
@@ -331,12 +338,42 @@ async function readPostgresIndexes(db: BaseDatabase, tableName: string, pgSchema
331
338
 
332
339
  return rows.map(row => ({
333
340
  name: row.index_name,
334
- columns: Array.isArray(row.columns) ? row.columns : [row.columns],
341
+ columns: parsePgTextArray(row.columns),
335
342
  unique: row.is_unique,
336
343
  spatial: false
337
344
  }));
338
345
  }
339
346
 
347
+ /** Parse a Postgres text array literal like `{a,b,"c d"}` into a JS string array.
348
+ * Accepts already-parsed arrays unchanged. */
349
+ function parsePgTextArray(value: unknown): string[] {
350
+ if (Array.isArray(value)) return value as string[];
351
+ if (typeof value !== 'string') return [];
352
+ const s = value.trim();
353
+ if (!s.startsWith('{') || !s.endsWith('}')) return [s];
354
+ const inner = s.slice(1, -1);
355
+ if (inner === '') return [];
356
+ // Split on commas not inside quotes; strip surrounding double quotes if present
357
+ const out: string[] = [];
358
+ let cur = '';
359
+ let inQuote = false;
360
+ for (let i = 0; i < inner.length; i++) {
361
+ const ch = inner[i];
362
+ if (ch === '"' && inner[i - 1] !== '\\') {
363
+ inQuote = !inQuote;
364
+ continue;
365
+ }
366
+ if (ch === ',' && !inQuote) {
367
+ out.push(cur);
368
+ cur = '';
369
+ continue;
370
+ }
371
+ cur += ch;
372
+ }
373
+ if (cur.length > 0 || out.length > 0) out.push(cur);
374
+ return out;
375
+ }
376
+
340
377
  async function readPostgresForeignKeys(db: BaseDatabase, tableName: string, pgSchema: string = 'public'): Promise<ForeignKeySchema[]> {
341
378
  // Use pg_constraint to correctly pair multi-column FK columns via conkey/confkey arrays
342
379
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -371,9 +408,9 @@ async function readPostgresForeignKeys(db: BaseDatabase, tableName: string, pgSc
371
408
 
372
409
  return rows.map(row => ({
373
410
  name: row.constraint_name,
374
- columns: Array.isArray(row.columns) ? row.columns : [row.columns],
411
+ columns: parsePgTextArray(row.columns),
375
412
  referencedTable: row.referenced_table,
376
- referencedColumns: Array.isArray(row.referenced_columns) ? row.referenced_columns : [row.referenced_columns],
413
+ referencedColumns: parsePgTextArray(row.referenced_columns),
377
414
  onDelete: (row.delete_rule || 'RESTRICT').toUpperCase(),
378
415
  onUpdate: (row.update_rule || 'RESTRICT').toUpperCase()
379
416
  }));