@hed-hog/cli 0.0.94 → 0.0.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/app.module.js +2 -0
- package/dist/src/app.module.js.map +1 -1
- package/dist/src/commands/new.command.d.ts +6 -0
- package/dist/src/commands/new.command.js +198 -93
- package/dist/src/commands/new.command.js.map +1 -1
- package/dist/src/commands/update.command.d.ts +14 -0
- package/dist/src/commands/update.command.js +58 -0
- package/dist/src/commands/update.command.js.map +1 -0
- package/dist/src/modules/database/database.service.d.ts +8 -0
- package/dist/src/modules/database/database.service.js +120 -0
- package/dist/src/modules/database/database.service.js.map +1 -1
- package/dist/src/modules/hedhog/hedhog.module.js +5 -2
- package/dist/src/modules/hedhog/hedhog.module.js.map +1 -1
- package/dist/src/modules/hedhog/hedhog.service.d.ts +12 -0
- package/dist/src/modules/hedhog/hedhog.service.js +178 -9
- package/dist/src/modules/hedhog/hedhog.service.js.map +1 -1
- package/dist/src/modules/hedhog/services/diff.service.d.ts +107 -0
- package/dist/src/modules/hedhog/services/diff.service.js +573 -0
- package/dist/src/modules/hedhog/services/diff.service.js.map +1 -0
- package/dist/src/modules/hedhog/services/migration.service.d.ts +36 -1
- package/dist/src/modules/hedhog/services/migration.service.js +619 -11
- package/dist/src/modules/hedhog/services/migration.service.js.map +1 -1
- package/dist/src/modules/hedhog/services/table.service.d.ts +7 -0
- package/dist/src/modules/hedhog/services/table.service.js +93 -13
- package/dist/src/modules/hedhog/services/table.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ const path_1 = require("path");
|
|
|
17
17
|
const sql_formatter_1 = require("sql-formatter");
|
|
18
18
|
const database_service_1 = require("../../database/database.service");
|
|
19
19
|
const developer_service_1 = require("../../developer/developer.service");
|
|
20
|
+
const diff_service_1 = require("./diff.service");
|
|
20
21
|
const file_system_service_1 = require("./file-system.service");
|
|
21
22
|
const table_service_1 = require("./table.service");
|
|
22
23
|
let MigrationService = class MigrationService {
|
|
@@ -24,14 +25,16 @@ let MigrationService = class MigrationService {
|
|
|
24
25
|
developer;
|
|
25
26
|
fileSystem;
|
|
26
27
|
tableService;
|
|
28
|
+
diffService;
|
|
27
29
|
indexWithClauseLocale = 0;
|
|
28
30
|
touchUpdatedAtCreated = false;
|
|
29
31
|
verbose = false;
|
|
30
|
-
constructor(database, developer, fileSystem, tableService) {
|
|
32
|
+
constructor(database, developer, fileSystem, tableService, diffService) {
|
|
31
33
|
this.database = database;
|
|
32
34
|
this.developer = developer;
|
|
33
35
|
this.fileSystem = fileSystem;
|
|
34
36
|
this.tableService = tableService;
|
|
37
|
+
this.diffService = diffService;
|
|
35
38
|
}
|
|
36
39
|
log(message) {
|
|
37
40
|
if (this.verbose) {
|
|
@@ -171,6 +174,13 @@ let MigrationService = class MigrationService {
|
|
|
171
174
|
getEnumOrigin(table, column) {
|
|
172
175
|
return `${table.name}.${column.name}`;
|
|
173
176
|
}
|
|
177
|
+
getOwnedTablesForMigration(tables, dependencyTables) {
|
|
178
|
+
if (!dependencyTables.length) {
|
|
179
|
+
return tables;
|
|
180
|
+
}
|
|
181
|
+
const dependencyTableNames = new Set(dependencyTables.map((table) => table.name));
|
|
182
|
+
return tables.filter((table) => !dependencyTableNames.has(table.name));
|
|
183
|
+
}
|
|
174
184
|
getEnumName(tableName, columnName) {
|
|
175
185
|
const hash = (0, crypto_1.createHash)('sha1')
|
|
176
186
|
.update(`${tableName}:${columnName}`)
|
|
@@ -304,6 +314,64 @@ let MigrationService = class MigrationService {
|
|
|
304
314
|
}
|
|
305
315
|
return transformedContent;
|
|
306
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Makes SQL statements idempotent using standard DDL (no DO blocks, which
|
|
319
|
+
* the sql-formatter breaks apart).
|
|
320
|
+
*
|
|
321
|
+
* Handles three cases for after-query SQL files:
|
|
322
|
+
*
|
|
323
|
+
* 1. ALTER COLUMN ... TYPE blocked by views:
|
|
324
|
+
* If the file contains ALTER COLUMN ... TYPE AND defines views with
|
|
325
|
+
* CREATE [OR REPLACE] VIEW, prepend DROP VIEW IF EXISTS for each view so
|
|
326
|
+
* the ALTER succeeds. The views are recreated by the CREATE OR REPLACE
|
|
327
|
+
* VIEW statements already in the same file.
|
|
328
|
+
*
|
|
329
|
+
* 2. ALTER TABLE … ADD CONSTRAINT <name> (bare, not inside a DO block):
|
|
330
|
+
* → prepend DROP CONSTRAINT IF EXISTS <name>
|
|
331
|
+
*
|
|
332
|
+
* 3. CREATE [UNIQUE] INDEX <name> without IF NOT EXISTS:
|
|
333
|
+
* → insert IF NOT EXISTS
|
|
334
|
+
*/
|
|
335
|
+
wrapIdempotentSql(sql) {
|
|
336
|
+
const stripped = sql.replace(/--[^\n]*/g, '').trim();
|
|
337
|
+
let result = sql;
|
|
338
|
+
// ── Case 1: ALTER COLUMN ... TYPE / view dependency ──────────────────────
|
|
339
|
+
// If the file alters a column's type AND defines views, those views likely
|
|
340
|
+
// depend on the column. Drop them first; CREATE OR REPLACE VIEW later in
|
|
341
|
+
// the file will recreate them.
|
|
342
|
+
if (/\bALTER\s+COLUMN\b.+?\bTYPE\s+\w/is.test(stripped)) {
|
|
343
|
+
const viewNames = [];
|
|
344
|
+
const viewRegex = /\bCREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+"?(\w+)"?/gi;
|
|
345
|
+
let m;
|
|
346
|
+
while ((m = viewRegex.exec(stripped)) !== null) {
|
|
347
|
+
viewNames.push(m[1]);
|
|
348
|
+
}
|
|
349
|
+
if (viewNames.length > 0) {
|
|
350
|
+
const drops = viewNames
|
|
351
|
+
.map((v) => `DROP VIEW IF EXISTS "${v}";`)
|
|
352
|
+
.join('\n');
|
|
353
|
+
result = `${drops}\n\n${result}`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// ── Case 2: Bare ADD CONSTRAINT (not already inside a DO $$ block) ───────
|
|
357
|
+
// Files that have DO $$ guards around constraints are already idempotent.
|
|
358
|
+
const hasDOBlocks = /\bDO\s*\$\$/.test(stripped);
|
|
359
|
+
if (!hasDOBlocks) {
|
|
360
|
+
const constraintMatch = /ALTER\s+TABLE\s+"?(\w+)"?\s+ADD\s+CONSTRAINT\s+"?(\w+)"?/is.exec(stripped);
|
|
361
|
+
if (constraintMatch) {
|
|
362
|
+
const table = constraintMatch[1];
|
|
363
|
+
const constraint = constraintMatch[2];
|
|
364
|
+
const drop = `ALTER TABLE "${table}" DROP CONSTRAINT IF EXISTS "${constraint}";`;
|
|
365
|
+
result = `${drop}\n${result.trim()}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ── Case 3: CREATE [UNIQUE] INDEX without IF NOT EXISTS ───────────────────
|
|
369
|
+
if (/\bCREATE\b.*\bINDEX\b/is.test(stripped) &&
|
|
370
|
+
!/\bIF\s+NOT\s+EXISTS\b/is.test(stripped)) {
|
|
371
|
+
result = result.replace(/\bCREATE(\s+UNIQUE)?\s+INDEX\b/gi, (_m, unique) => `CREATE${unique ?? ''} INDEX IF NOT EXISTS`);
|
|
372
|
+
}
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
307
375
|
normalizeCustomSqlQuery(file, sqlContent, enumRegistry, columnRegistry) {
|
|
308
376
|
const normalizedDeclarations = this.normalizeCustomSqlEnumDeclarations(file, sqlContent, enumRegistry, columnRegistry);
|
|
309
377
|
return this.normalizeCustomSqlAgainstSchema(file, normalizedDeclarations, columnRegistry);
|
|
@@ -567,12 +635,12 @@ let MigrationService = class MigrationService {
|
|
|
567
635
|
this.log(`Creating unique constraints for table: ${table.name}`);
|
|
568
636
|
const uniqueColumns = table.columns.filter((c) => c.isUnique);
|
|
569
637
|
if (uniqueColumns.length) {
|
|
570
|
-
migrationContent.push(`CREATE UNIQUE INDEX "${table.name}_${uniqueColumns.map((c) => c.name).join('_')}_unique" ON "${table.name}" (${uniqueColumns.map((c) => `"${c.name}"`).join(', ')});`, ``);
|
|
638
|
+
migrationContent.push(`CREATE UNIQUE INDEX IF NOT EXISTS "${table.name}_${uniqueColumns.map((c) => c.name).join('_')}_unique" ON "${table.name}" (${uniqueColumns.map((c) => `"${c.name}"`).join(', ')});`, ``);
|
|
571
639
|
}
|
|
572
640
|
for (const column of table.columns) {
|
|
573
641
|
if (column.type === 'slug') {
|
|
574
642
|
this.log(`Adding unique constraint for column: ${column.name}`);
|
|
575
|
-
migrationContent.push(`CREATE UNIQUE INDEX "${table.name}_${column.name}_unique" ON "${table.name}" ("${column.name}");`, ``);
|
|
643
|
+
migrationContent.push(`CREATE UNIQUE INDEX IF NOT EXISTS "${table.name}_${column.name}_unique" ON "${table.name}" ("${column.name}");`, ``);
|
|
576
644
|
}
|
|
577
645
|
}
|
|
578
646
|
}
|
|
@@ -837,9 +905,9 @@ let MigrationService = class MigrationService {
|
|
|
837
905
|
}
|
|
838
906
|
return [];
|
|
839
907
|
}
|
|
840
|
-
async afterQueries(name, cwd, migrationContent, enumRegistry, columnRegistry) {
|
|
908
|
+
async afterQueries(name, cwd, migrationContent, enumRegistry, columnRegistry, customQueriesPath) {
|
|
841
909
|
this.log(`Running after queries for ${name}...`);
|
|
842
|
-
const queriesPath = (0, path_1.resolve)(cwd, `./libraries/${name}/hedhog/query`);
|
|
910
|
+
const queriesPath = customQueriesPath ?? (0, path_1.resolve)(cwd, `./libraries/${name}/hedhog/query`);
|
|
843
911
|
try {
|
|
844
912
|
if (await this.fileSystem.exists(queriesPath)) {
|
|
845
913
|
const allFiles = await this.fileSystem.listFiles(queriesPath, () => true);
|
|
@@ -847,7 +915,7 @@ let MigrationService = class MigrationService {
|
|
|
847
915
|
if (file.endsWith('.sql')) {
|
|
848
916
|
const sqlContent = await this.fileSystem.readFileContent((0, path_1.join)(queriesPath, file));
|
|
849
917
|
const normalizedSql = this.normalizeCustomSqlQuery(file, sqlContent, enumRegistry, columnRegistry);
|
|
850
|
-
migrationContent.push(`-- AfterQuery: ${file}`, normalizedSql, '');
|
|
918
|
+
migrationContent.push(`-- AfterQuery: ${file}`, this.wrapIdempotentSql(normalizedSql), '');
|
|
851
919
|
}
|
|
852
920
|
else {
|
|
853
921
|
console.warn(chalk.yellow(`Arquivo ignorado no diretório de queries: ${file}`));
|
|
@@ -936,6 +1004,7 @@ let MigrationService = class MigrationService {
|
|
|
936
1004
|
const allTables = [];
|
|
937
1005
|
const tables = await this.tableService.loadTablesFromYaml(name, cwd, verbose);
|
|
938
1006
|
const tablesDep = await this.tableService.getTablesFromDependencies(name, cwd);
|
|
1007
|
+
const ownedTables = this.getOwnedTablesForMigration(tables, tablesDep);
|
|
939
1008
|
allTables.push(...tablesDep.filter((t) => !tables.find((tbl) => tbl.name === t.name)));
|
|
940
1009
|
const uniqueTablesMap = new Map();
|
|
941
1010
|
[...allTables, ...tables].forEach((table) => {
|
|
@@ -981,17 +1050,23 @@ let MigrationService = class MigrationService {
|
|
|
981
1050
|
const enumRegistry = this.buildEnumRegistry(allTables);
|
|
982
1051
|
const columnRegistry = this.buildColumnTypeRegistry(allTables);
|
|
983
1052
|
this.log(`Database type detected: ${dbType}`);
|
|
1053
|
+
if (ownedTables.length !== tables.length) {
|
|
1054
|
+
const skippedTables = tables
|
|
1055
|
+
.filter((table) => !ownedTables.some((owned) => owned.name === table.name))
|
|
1056
|
+
.map((table) => table.name);
|
|
1057
|
+
this.log(`Skipping schema creation for dependency-owned tables in ${name}: ${skippedTables.join(', ')}`);
|
|
1058
|
+
}
|
|
984
1059
|
this.log(`Processing enums for ${name}...`);
|
|
985
|
-
this.processEnums(
|
|
1060
|
+
this.processEnums(ownedTables, dbType, migrationContent, enumRegistry);
|
|
986
1061
|
this.log(`Processed enums for ${name}!`);
|
|
987
1062
|
this.log(`Creating tables for ${name}...`);
|
|
988
|
-
await this.createTableMigrations(
|
|
1063
|
+
await this.createTableMigrations(ownedTables, dbType, migrationContent, createdTables, cwd);
|
|
989
1064
|
this.log(`Created tables for ${name}!`);
|
|
990
1065
|
this.log(`Adding unique constraints for ${name}...`);
|
|
991
|
-
this.addUniqueConstraints(
|
|
1066
|
+
this.addUniqueConstraints(ownedTables, dbType, migrationContent);
|
|
992
1067
|
this.log(`Added unique constraints for ${name}!`);
|
|
993
1068
|
this.log(`Adding foreign keys for ${name}...`);
|
|
994
|
-
this.addForeignKeys(
|
|
1069
|
+
this.addForeignKeys(ownedTables, dbType, migrationContent);
|
|
995
1070
|
this.log(`Added foreign keys for ${name}!`);
|
|
996
1071
|
this.log(`Inserting initial data for ${name}...`);
|
|
997
1072
|
await this.insertInitialData(!allTables.length && tables.length ? tables : allTables, name, cwd, migrationContent);
|
|
@@ -1038,6 +1113,538 @@ let MigrationService = class MigrationService {
|
|
|
1038
1113
|
await this.fileSystem.writeJsonFile(hedhogFilePath, hedhogFile);
|
|
1039
1114
|
this.log(`Migration file ${migrationFileName} created successfully.`);
|
|
1040
1115
|
}
|
|
1116
|
+
// ── Update migration ─────────────────────────────────────────────────────────
|
|
1117
|
+
/**
|
|
1118
|
+
* Generate a differential migration (ALTER TABLE + upserts) for an existing
|
|
1119
|
+
* library being updated. Reads old schema/data from `libraries/<name>/` and
|
|
1120
|
+
* new schema/data from `libraries/<name>/.hedhog-update/`.
|
|
1121
|
+
*/
|
|
1122
|
+
async createUpdateMigrations(name, cwd = process.cwd(), verbose = false) {
|
|
1123
|
+
this.verbose = verbose;
|
|
1124
|
+
const oldLibraryPath = (0, path_1.resolve)(cwd, `./libraries/${name}`);
|
|
1125
|
+
const newLibraryPath = (0, path_1.join)(oldLibraryPath, '.hedhog-update');
|
|
1126
|
+
this.log(`Loading old table definitions from ${oldLibraryPath}...`);
|
|
1127
|
+
const oldTables = await this.tableService.loadLibraryOwnedTables(oldLibraryPath, verbose);
|
|
1128
|
+
this.log(`Loading new table definitions from ${newLibraryPath}...`);
|
|
1129
|
+
const newTables = await this.tableService.loadLibraryOwnedTables(newLibraryPath, verbose);
|
|
1130
|
+
// Build full allTables for FK/locale resolution during SQL generation
|
|
1131
|
+
// Use new version's dependencies since it may have updated deps
|
|
1132
|
+
const tablesDep = await this.tableService.getTablesFromDependencies(name, cwd);
|
|
1133
|
+
const allTablesMap = new Map();
|
|
1134
|
+
for (const t of [...tablesDep, ...newTables]) {
|
|
1135
|
+
if (!allTablesMap.has(t.name))
|
|
1136
|
+
allTablesMap.set(t.name, t);
|
|
1137
|
+
}
|
|
1138
|
+
const allTables = [...allTablesMap.values()];
|
|
1139
|
+
// Diffs
|
|
1140
|
+
const schemaDiff = this.diffService.diffSchema(oldTables, newTables);
|
|
1141
|
+
const dataDiff = await this.diffService.diffData(oldLibraryPath, newLibraryPath, allTablesMap);
|
|
1142
|
+
// ── Interactive prompts ────────────────────────────────────────────────────
|
|
1143
|
+
// Resolve renames for changed tables
|
|
1144
|
+
for (const tableChanges of schemaDiff.changedTables) {
|
|
1145
|
+
const candidates = this.diffService.detectRenameCandidates(tableChanges.removedColumns, tableChanges.addedColumns);
|
|
1146
|
+
if (candidates.length > 0) {
|
|
1147
|
+
const decisions = await this.diffService.promptRenames(tableChanges.tableName, candidates);
|
|
1148
|
+
tableChanges.renamedColumns = decisions.filter((d) => d.isRename);
|
|
1149
|
+
// Remove confirmed renames from added/removed lists
|
|
1150
|
+
const renamedOldNames = new Set(tableChanges.renamedColumns.map((r) => r.oldName));
|
|
1151
|
+
const renamedNewNames = new Set(tableChanges.renamedColumns.map((r) => r.newName));
|
|
1152
|
+
tableChanges.removedColumns = tableChanges.removedColumns.filter((c) => !renamedOldNames.has(c.name));
|
|
1153
|
+
tableChanges.addedColumns = tableChanges.addedColumns.filter((c) => !renamedNewNames.has(c.name));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
// Prompt for dropped columns
|
|
1157
|
+
const approvedDropsMap = new Map();
|
|
1158
|
+
for (const tableChanges of schemaDiff.changedTables) {
|
|
1159
|
+
if (tableChanges.removedColumns.length > 0) {
|
|
1160
|
+
const approved = await this.diffService.promptDropColumns(tableChanges.tableName, tableChanges.removedColumns);
|
|
1161
|
+
approvedDropsMap.set(tableChanges.tableName, approved);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
// Prompt for removed data rows
|
|
1165
|
+
const approvedDeletesMap = new Map();
|
|
1166
|
+
for (const change of dataDiff.changes) {
|
|
1167
|
+
if (change.removedRows.length > 0) {
|
|
1168
|
+
const approved = await this.diffService.promptDeleteRows(change.tableName, change.removedRows, change.keyColumn);
|
|
1169
|
+
approvedDeletesMap.set(change.tableName, approved);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
// Prompt for entirely removed data files
|
|
1173
|
+
const approvedFileDeletesMap = new Map();
|
|
1174
|
+
for (const fc of dataDiff.fileChanges) {
|
|
1175
|
+
if (fc.isRemoved) {
|
|
1176
|
+
const decision = await this.diffService.promptDeleteEntireDataFile(fc.tableName, fc.allRows);
|
|
1177
|
+
approvedFileDeletesMap.set(fc.tableName, decision);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
// Prompt for removed relations
|
|
1181
|
+
const approvedRelationDeletesMap = new Map();
|
|
1182
|
+
for (const change of dataDiff.changes) {
|
|
1183
|
+
for (const relChange of change.relationChanges) {
|
|
1184
|
+
if (relChange.removedRelations.length > 0) {
|
|
1185
|
+
const pairs = relChange.removedRelations.map((rel) => ({
|
|
1186
|
+
parentKeyValue: relChange.parentKeyValue,
|
|
1187
|
+
relation: rel,
|
|
1188
|
+
}));
|
|
1189
|
+
const mapKey = `${change.tableName}::${relChange.targetTable}`;
|
|
1190
|
+
const approved = await this.diffService.promptDeleteRelations(change.tableName, relChange.targetTable, pairs);
|
|
1191
|
+
approvedRelationDeletesMap.set(mapKey, approved);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// ── SQL generation ─────────────────────────────────────────────────────────
|
|
1196
|
+
const dbType = await this.database.getDatabaseType(cwd);
|
|
1197
|
+
if (!dbType)
|
|
1198
|
+
throw new Error('Database type could not be determined');
|
|
1199
|
+
const enumRegistry = this.buildEnumRegistry(allTables);
|
|
1200
|
+
const migrationContent = [];
|
|
1201
|
+
// 1. New tables (full CREATE TABLE pipeline)
|
|
1202
|
+
if (schemaDiff.newTables.length > 0) {
|
|
1203
|
+
const newEnumRegistry = this.buildEnumRegistry(schemaDiff.newTables);
|
|
1204
|
+
this.processEnums(schemaDiff.newTables, dbType, migrationContent, newEnumRegistry);
|
|
1205
|
+
const createdTables = [];
|
|
1206
|
+
await this.createTableMigrations(schemaDiff.newTables, dbType, migrationContent, createdTables, cwd);
|
|
1207
|
+
this.addUniqueConstraints(schemaDiff.newTables, dbType, migrationContent);
|
|
1208
|
+
this.addForeignKeys(schemaDiff.newTables, dbType, migrationContent);
|
|
1209
|
+
}
|
|
1210
|
+
// 2. Removed tables (warning comment only)
|
|
1211
|
+
for (const removed of schemaDiff.removedTables) {
|
|
1212
|
+
migrationContent.push(`-- WARNING: Table "${removed.name}" was removed from the library YAML but was NOT dropped from the database. Manual action may be required.`, '');
|
|
1213
|
+
}
|
|
1214
|
+
// 3. ALTER TABLE statements for changed tables
|
|
1215
|
+
for (const tableChanges of schemaDiff.changedTables) {
|
|
1216
|
+
const approvedDrops = approvedDropsMap.get(tableChanges.tableName) ?? [];
|
|
1217
|
+
const alterSql = this.generateAlterTableSQL(tableChanges, approvedDrops, enumRegistry, dbType, cwd);
|
|
1218
|
+
migrationContent.push(...alterSql);
|
|
1219
|
+
}
|
|
1220
|
+
// 4. Data upserts / deletes
|
|
1221
|
+
const dataSql = await this.generateDataUpdateSQL(dataDiff, allTables, approvedDeletesMap, approvedFileDeletesMap, approvedRelationDeletesMap, cwd);
|
|
1222
|
+
migrationContent.push(...dataSql);
|
|
1223
|
+
// 5. After queries (new version's hedhog/query/*.sql)
|
|
1224
|
+
const columnRegistry = this.buildColumnTypeRegistry(allTables);
|
|
1225
|
+
await this.afterQueries(name, cwd, migrationContent, enumRegistry, columnRegistry, (0, path_1.join)(newLibraryPath, 'hedhog', 'query'));
|
|
1226
|
+
// ── Write migration file ──────────────────────────────────────────────────
|
|
1227
|
+
const hasMeaningfulContent = migrationContent.some((line) => line.trim() && !line.startsWith('--'));
|
|
1228
|
+
if (!hasMeaningfulContent) {
|
|
1229
|
+
this.log(`No schema or data changes detected for ${name}. Skipping migration file.`);
|
|
1230
|
+
console.info(chalk.blue(`[update] No changes detected for "${name}". Migration file not created.`));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const timestamp = new Date()
|
|
1234
|
+
.toISOString()
|
|
1235
|
+
.replace(/[-:.T]/g, '')
|
|
1236
|
+
.slice(0, 14);
|
|
1237
|
+
const migrationFileName = `${timestamp}_update_${this.toSnackCase(name)}`;
|
|
1238
|
+
const migrationFilePath = (0, path_1.resolve)(cwd, `./apps/api/prisma/migrations/${migrationFileName}`);
|
|
1239
|
+
await this.fileSystem.createDirectory(migrationFilePath);
|
|
1240
|
+
const formattedSQL = (0, sql_formatter_1.format)(migrationContent.join('\n') + '\n', {
|
|
1241
|
+
language: 'postgresql',
|
|
1242
|
+
newlineBeforeSemicolon: false,
|
|
1243
|
+
});
|
|
1244
|
+
await this.fileSystem.writeFileContent((0, path_1.resolve)(migrationFilePath, 'migration.sql'), formattedSQL);
|
|
1245
|
+
// Update hedhog.json hash/migration entry
|
|
1246
|
+
const hedhogFilePath = (0, path_1.join)(cwd, 'hedhog.json');
|
|
1247
|
+
let hedhogFile = {};
|
|
1248
|
+
if (await this.fileSystem.exists(hedhogFilePath)) {
|
|
1249
|
+
hedhogFile = await this.fileSystem.readJsonFile(hedhogFilePath);
|
|
1250
|
+
}
|
|
1251
|
+
if (!hedhogFile.libraries)
|
|
1252
|
+
hedhogFile.libraries = {};
|
|
1253
|
+
// Hash the NEW version (still in .hedhog-update/) — hedhog.service.ts will
|
|
1254
|
+
// replace the library folder AFTER this method returns.
|
|
1255
|
+
const hashLib = await this.developer.hashDirectory(newLibraryPath);
|
|
1256
|
+
hedhogFile.libraries[name] = {
|
|
1257
|
+
hash: hashLib,
|
|
1258
|
+
migration: migrationFileName,
|
|
1259
|
+
};
|
|
1260
|
+
await this.fileSystem.writeJsonFile(hedhogFilePath, hedhogFile);
|
|
1261
|
+
this.log(`Update migration file ${migrationFileName} created successfully.`);
|
|
1262
|
+
}
|
|
1263
|
+
// ── ALTER TABLE SQL generation ────────────────────────────────────────────
|
|
1264
|
+
generateAlterTableSQL(changes, approvedDrops, enumRegistry, dbType, cwd) {
|
|
1265
|
+
if (dbType !== 'postgres')
|
|
1266
|
+
return [];
|
|
1267
|
+
const sql = [];
|
|
1268
|
+
const t = changes.tableName;
|
|
1269
|
+
// RENAMES (before any other operation on those columns)
|
|
1270
|
+
for (const rename of changes.renamedColumns.filter((r) => r.isRename)) {
|
|
1271
|
+
sql.push(`-- RenameColumn`, `ALTER TABLE "${t}" RENAME COLUMN "${rename.oldName}" TO "${rename.newName}";`, '');
|
|
1272
|
+
// If type/nullable/default also changed, apply those after rename
|
|
1273
|
+
const change = changes.changedColumns.find((c) => c.column.name === rename.newName);
|
|
1274
|
+
if (change) {
|
|
1275
|
+
sql.push(...this.generateColumnAlterSQL(t, change, enumRegistry));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const renamedNewNames = new Set(changes.renamedColumns.filter((r) => r.isRename).map((r) => r.newName));
|
|
1279
|
+
// ADD COLUMNS
|
|
1280
|
+
for (const col of changes.addedColumns) {
|
|
1281
|
+
const typeDef = this.getColumnTypeDef(col, t, enumRegistry);
|
|
1282
|
+
const nullable = col.isNullable ? 'NULL' : 'NOT NULL';
|
|
1283
|
+
const defaultClause = col.default !== undefined
|
|
1284
|
+
? ` DEFAULT ${this.getDefaultValue(col)}`
|
|
1285
|
+
: '';
|
|
1286
|
+
sql.push(`-- AddColumn`, `ALTER TABLE "${t}" ADD COLUMN "${col.name}" ${typeDef} ${nullable}${defaultClause};`, '');
|
|
1287
|
+
}
|
|
1288
|
+
// DROP COLUMNS (approved only)
|
|
1289
|
+
for (const col of approvedDrops) {
|
|
1290
|
+
sql.push(`-- DropColumn`, `ALTER TABLE "${t}" DROP COLUMN "${col.name}";`, '');
|
|
1291
|
+
}
|
|
1292
|
+
// CHANGED COLUMNS (skip ones that were part of a rename — already handled)
|
|
1293
|
+
for (const change of changes.changedColumns) {
|
|
1294
|
+
if (renamedNewNames.has(change.column.name))
|
|
1295
|
+
continue;
|
|
1296
|
+
sql.push(...this.generateColumnAlterSQL(t, change, enumRegistry));
|
|
1297
|
+
}
|
|
1298
|
+
// NEW UNIQUE CONSTRAINTS
|
|
1299
|
+
for (const colName of changes.addedUniqueConstraints) {
|
|
1300
|
+
sql.push(`-- CreateUniqueIndex`, `CREATE UNIQUE INDEX IF NOT EXISTS "${t}_${colName}_unique" ON "${t}" ("${colName}");`, '');
|
|
1301
|
+
}
|
|
1302
|
+
// DROPPED UNIQUE CONSTRAINTS
|
|
1303
|
+
for (const colName of changes.removedUniqueConstraints) {
|
|
1304
|
+
sql.push(`-- DropUniqueIndex`, `DROP INDEX IF EXISTS "${t}_${colName}_unique";`, '');
|
|
1305
|
+
}
|
|
1306
|
+
// NEW FOREIGN KEYS
|
|
1307
|
+
for (const col of changes.addedForeignKeys) {
|
|
1308
|
+
const { table: fkTable, column: fkCol, onDelete, onUpdate, } = col.references;
|
|
1309
|
+
sql.push(`-- AddForeignKey`, `ALTER TABLE "${t}" ADD CONSTRAINT "${fkTable}_${col.name}_fkey" FOREIGN KEY ("${col.name}") REFERENCES "${fkTable}" ("${fkCol}") ON DELETE ${onDelete || 'NO ACTION'} ON UPDATE ${onUpdate || 'NO ACTION'};`, '');
|
|
1310
|
+
}
|
|
1311
|
+
// DROPPED FOREIGN KEYS
|
|
1312
|
+
for (const col of changes.removedForeignKeys) {
|
|
1313
|
+
const { table: fkTable } = col.references;
|
|
1314
|
+
sql.push(`-- DropForeignKey`, `ALTER TABLE "${t}" DROP CONSTRAINT "${fkTable}_${col.name}_fkey";`, '');
|
|
1315
|
+
}
|
|
1316
|
+
// ENUM VALUE CHANGES
|
|
1317
|
+
for (const enumChange of changes.enumValueChanges) {
|
|
1318
|
+
const enumName = this.getEnumName(t, enumChange.columnName);
|
|
1319
|
+
for (const val of enumChange.addedValues) {
|
|
1320
|
+
sql.push(`-- AddEnumValue`, `ALTER TYPE "${enumName}" ADD VALUE IF NOT EXISTS '${this.escapeSqlLiteral(val)}';`, '');
|
|
1321
|
+
}
|
|
1322
|
+
for (const val of enumChange.removedValues) {
|
|
1323
|
+
sql.push(`-- WARNING: Cannot remove enum value '${val}' from type "${enumName}". PostgreSQL does not support removing enum values directly. Manual migration required (recreate type, update column, drop old type).`, '');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return sql;
|
|
1327
|
+
}
|
|
1328
|
+
generateColumnAlterSQL(tableName, change, enumRegistry) {
|
|
1329
|
+
const sql = [];
|
|
1330
|
+
const col = change.column;
|
|
1331
|
+
const colName = col.name;
|
|
1332
|
+
if (change.oldType !== change.newType ||
|
|
1333
|
+
change.oldLength !== change.newLength) {
|
|
1334
|
+
const newTypeDef = this.getColumnTypeDef(col, tableName, enumRegistry);
|
|
1335
|
+
sql.push(`-- AlterColumnType`, `ALTER TABLE "${tableName}" ALTER COLUMN "${colName}" TYPE ${newTypeDef} USING "${colName}"::TEXT::${newTypeDef};`, '');
|
|
1336
|
+
}
|
|
1337
|
+
if (change.oldNullable !== change.newNullable) {
|
|
1338
|
+
if (change.newNullable) {
|
|
1339
|
+
sql.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${colName}" DROP NOT NULL;`, '');
|
|
1340
|
+
}
|
|
1341
|
+
else {
|
|
1342
|
+
sql.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${colName}" SET NOT NULL;`, '');
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const oldDef = String(change.oldDefault ?? '');
|
|
1346
|
+
const newDef = String(change.newDefault ?? '');
|
|
1347
|
+
if (oldDef !== newDef) {
|
|
1348
|
+
if (change.newDefault === undefined || change.newDefault === null) {
|
|
1349
|
+
sql.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${colName}" DROP DEFAULT;`, '');
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
sql.push(`ALTER TABLE "${tableName}" ALTER COLUMN "${colName}" SET DEFAULT ${this.getDefaultValue(col)};`, '');
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return sql;
|
|
1356
|
+
}
|
|
1357
|
+
getColumnTypeDef(col, tableName, enumRegistry) {
|
|
1358
|
+
switch (col.type.toLowerCase()) {
|
|
1359
|
+
case 'pk':
|
|
1360
|
+
return 'SERIAL';
|
|
1361
|
+
case 'fk':
|
|
1362
|
+
case 'int':
|
|
1363
|
+
case 'order':
|
|
1364
|
+
return 'INTEGER';
|
|
1365
|
+
case 'slug':
|
|
1366
|
+
case 'varchar':
|
|
1367
|
+
return `VARCHAR(${col.length || 255})`;
|
|
1368
|
+
case 'created_at':
|
|
1369
|
+
case 'updated_at':
|
|
1370
|
+
case 'datetime':
|
|
1371
|
+
return 'TIMESTAMPTZ';
|
|
1372
|
+
case 'locale_varchar':
|
|
1373
|
+
return `VARCHAR(${col.length || 255})`;
|
|
1374
|
+
case 'locale_text':
|
|
1375
|
+
return 'TEXT';
|
|
1376
|
+
case 'char':
|
|
1377
|
+
return `CHAR(${col.length || 1})`;
|
|
1378
|
+
case 'enum': {
|
|
1379
|
+
const enumName = this.getEnumName(tableName, col.name);
|
|
1380
|
+
return `"${enumName}"`;
|
|
1381
|
+
}
|
|
1382
|
+
default:
|
|
1383
|
+
return col.type.toUpperCase();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
// ── Data upsert SQL generation ────────────────────────────────────────────
|
|
1387
|
+
async generateDataUpdateSQL(dataDiff, allTables, approvedDeletesMap, approvedFileDeletesMap, approvedRelationDeletesMap, cwd) {
|
|
1388
|
+
const sql = [];
|
|
1389
|
+
// Entirely new data files → all rows as upserts
|
|
1390
|
+
for (const fc of dataDiff.fileChanges) {
|
|
1391
|
+
if (fc.isNew) {
|
|
1392
|
+
const tableDef = allTables.find((t) => t.name === fc.tableName);
|
|
1393
|
+
if (!tableDef)
|
|
1394
|
+
continue;
|
|
1395
|
+
const keyCol = this.diffService.findIdentityKey(tableDef);
|
|
1396
|
+
if (!keyCol)
|
|
1397
|
+
continue;
|
|
1398
|
+
sql.push(`-- UpsertData (new data file): ${fc.tableName}`);
|
|
1399
|
+
for (const row of fc.allRows) {
|
|
1400
|
+
const rowSql = this.generateRowUpsertSQL(row, tableDef, keyCol, allTables);
|
|
1401
|
+
sql.push(...rowSql);
|
|
1402
|
+
}
|
|
1403
|
+
sql.push('');
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
// Entirely removed data files → DELETE if approved
|
|
1407
|
+
for (const fc of dataDiff.fileChanges) {
|
|
1408
|
+
if (fc.isRemoved) {
|
|
1409
|
+
const decision = approvedFileDeletesMap.get(fc.tableName);
|
|
1410
|
+
if (decision === 'delete') {
|
|
1411
|
+
const tableDef = allTables.find((t) => t.name === fc.tableName);
|
|
1412
|
+
const keyCol = tableDef
|
|
1413
|
+
? this.diffService.findIdentityKey(tableDef)
|
|
1414
|
+
: null;
|
|
1415
|
+
sql.push(`-- DeleteData (data file removed): ${fc.tableName}`);
|
|
1416
|
+
if (Array.isArray(keyCol)) {
|
|
1417
|
+
sql.push(`-- WARNING: composite key — DELETE skipped for "${fc.tableName}". Manual action may be required.`);
|
|
1418
|
+
}
|
|
1419
|
+
else if (keyCol) {
|
|
1420
|
+
for (const row of fc.allRows) {
|
|
1421
|
+
const keyVal = row[keyCol];
|
|
1422
|
+
if (keyVal !== null && keyVal !== undefined) {
|
|
1423
|
+
sql.push(`DELETE FROM "${fc.tableName}" WHERE "${keyCol}" = $$${String(keyVal).replace(/\$\$/g, "''")}$$;`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
sql.push(`-- WARNING: Could not determine identity key for "${fc.tableName}". DELETE skipped.`);
|
|
1429
|
+
}
|
|
1430
|
+
sql.push('');
|
|
1431
|
+
}
|
|
1432
|
+
else {
|
|
1433
|
+
sql.push(`-- INFO: Data file for "${fc.tableName}" was removed in the new version. Existing rows preserved.`, '');
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Row-level changes
|
|
1438
|
+
for (const change of dataDiff.changes) {
|
|
1439
|
+
const tableDef = allTables.find((t) => t.name === change.tableName);
|
|
1440
|
+
if (!tableDef)
|
|
1441
|
+
continue;
|
|
1442
|
+
// Inserted rows
|
|
1443
|
+
if (change.insertedRows.length > 0) {
|
|
1444
|
+
sql.push(`-- InsertData: ${change.tableName}`);
|
|
1445
|
+
for (const row of change.insertedRows) {
|
|
1446
|
+
sql.push(...this.generateRowUpsertSQL(row, tableDef, change.keyColumn, allTables));
|
|
1447
|
+
}
|
|
1448
|
+
sql.push('');
|
|
1449
|
+
}
|
|
1450
|
+
// Updated rows
|
|
1451
|
+
if (change.updatedRows.length > 0) {
|
|
1452
|
+
sql.push(`-- UpdateData: ${change.tableName}`);
|
|
1453
|
+
for (const row of change.updatedRows) {
|
|
1454
|
+
sql.push(...this.generateRowUpsertSQL(row, tableDef, change.keyColumn, allTables));
|
|
1455
|
+
}
|
|
1456
|
+
sql.push('');
|
|
1457
|
+
}
|
|
1458
|
+
// Deleted rows (approved)
|
|
1459
|
+
const approvedDeletes = approvedDeletesMap.get(change.tableName) ?? [];
|
|
1460
|
+
if (approvedDeletes.length > 0) {
|
|
1461
|
+
sql.push(`-- DeleteData: ${change.tableName}`);
|
|
1462
|
+
if (Array.isArray(change.keyColumn)) {
|
|
1463
|
+
sql.push(`-- WARNING: composite key — DELETE skipped for "${change.tableName}". Manual action may be required.`);
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
for (const row of approvedDeletes) {
|
|
1467
|
+
const keyVal = row[change.keyColumn];
|
|
1468
|
+
sql.push(`DELETE FROM "${change.tableName}" WHERE "${change.keyColumn}" = $$${String(keyVal).replace(/\$\$/g, "''")}$$;`);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
sql.push('');
|
|
1472
|
+
}
|
|
1473
|
+
// Relation changes
|
|
1474
|
+
for (const relChange of change.relationChanges) {
|
|
1475
|
+
if (Array.isArray(change.keyColumn))
|
|
1476
|
+
continue; // composite-key tables don't have nested relations
|
|
1477
|
+
const mapKey = `${change.tableName}::${relChange.targetTable}`;
|
|
1478
|
+
const approvedRelDeletes = approvedRelationDeletesMap.get(mapKey) ?? [];
|
|
1479
|
+
const relSql = await this.generateRelationUpdateSQL(change.tableName, change.keyColumn, relChange.parentKeyValue, relChange.targetTable, relChange.addedRelations, relChange.removedRelations, approvedRelDeletes, allTables, cwd);
|
|
1480
|
+
sql.push(...relSql);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return sql;
|
|
1484
|
+
}
|
|
1485
|
+
generateRowUpsertSQL(row, tableDef, keyColumn, allTables) {
|
|
1486
|
+
const sql = [];
|
|
1487
|
+
const keyCols = Array.isArray(keyColumn) ? keyColumn : [keyColumn];
|
|
1488
|
+
const keyColsQuoted = new Set(keyCols.map((k) => `"${k}"`));
|
|
1489
|
+
const scalarCols = [];
|
|
1490
|
+
const scalarVals = [];
|
|
1491
|
+
const localeEntries = [];
|
|
1492
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1493
|
+
if (key === 'relations')
|
|
1494
|
+
continue;
|
|
1495
|
+
if (this.checkIsObjectLocale(value)) {
|
|
1496
|
+
localeEntries.push({ name: key, values: value });
|
|
1497
|
+
}
|
|
1498
|
+
else if (tableDef.columns.some((c) => c.name === key)) {
|
|
1499
|
+
scalarCols.push(`"${key}"`);
|
|
1500
|
+
scalarVals.push(this.resolveRawValue(value, key, tableDef, allTables));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (scalarCols.length > 0) {
|
|
1504
|
+
if (keyCols.length > 1) {
|
|
1505
|
+
// Use WHERE NOT EXISTS — safe even without a DB-level unique constraint
|
|
1506
|
+
const whereConditions = keyCols
|
|
1507
|
+
.map((k) => {
|
|
1508
|
+
const idx = scalarCols.indexOf(`"${k}"`);
|
|
1509
|
+
const val = idx >= 0 ? scalarVals[idx] : 'NULL';
|
|
1510
|
+
return `"${k}" = ${val}`;
|
|
1511
|
+
})
|
|
1512
|
+
.join(' AND ');
|
|
1513
|
+
sql.push(`INSERT INTO "${tableDef.name}" (${scalarCols.join(', ')}) SELECT ${scalarVals.join(', ')} WHERE NOT EXISTS (SELECT 1 FROM "${tableDef.name}" WHERE ${whereConditions});`);
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
const conflictTarget = `"${keyCols[0]}"`;
|
|
1517
|
+
const setClauses = scalarCols
|
|
1518
|
+
.filter((c) => !keyColsQuoted.has(c))
|
|
1519
|
+
.map((c) => `${c} = EXCLUDED.${c}`)
|
|
1520
|
+
.join(', ');
|
|
1521
|
+
const doConflict = setClauses
|
|
1522
|
+
? `ON CONFLICT (${conflictTarget}) DO UPDATE SET ${setClauses}`
|
|
1523
|
+
: `ON CONFLICT (${conflictTarget}) DO NOTHING`;
|
|
1524
|
+
sql.push(`INSERT INTO "${tableDef.name}" (${scalarCols.join(', ')}) VALUES (${scalarVals.join(', ')}) ${doConflict};`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
// Locale upserts — only applicable when key is a single column
|
|
1528
|
+
const localeTableDef = allTables.find((t) => t.name === `${tableDef.name}_locale`);
|
|
1529
|
+
if (localeTableDef &&
|
|
1530
|
+
localeEntries.length > 0 &&
|
|
1531
|
+
!Array.isArray(keyColumn)) {
|
|
1532
|
+
const fkColName = localeTableDef.columns.find((c) => c.type === 'fk' && c.references?.table === tableDef.name)?.name;
|
|
1533
|
+
const fkLocaleColName = localeTableDef.columns.find((c) => c.type === 'fk' && c.references?.table === 'locale')?.name;
|
|
1534
|
+
if (fkColName && fkLocaleColName) {
|
|
1535
|
+
const keyVal = row[keyColumn];
|
|
1536
|
+
const parentKeySQL = this.resolveRawValue(keyVal, keyColumn, tableDef, allTables);
|
|
1537
|
+
const parentSelect = `(SELECT "${tableDef.pk}" FROM "${tableDef.name}" WHERE "${keyColumn}" = ${parentKeySQL})`;
|
|
1538
|
+
const localeCodes = new Set();
|
|
1539
|
+
for (const entry of localeEntries) {
|
|
1540
|
+
for (const code of Object.keys(entry.values))
|
|
1541
|
+
localeCodes.add(code);
|
|
1542
|
+
}
|
|
1543
|
+
for (const code of localeCodes) {
|
|
1544
|
+
const localeCols2 = [];
|
|
1545
|
+
const localeVals = [];
|
|
1546
|
+
for (const entry of localeEntries) {
|
|
1547
|
+
if (entry.values[code] !== undefined) {
|
|
1548
|
+
localeCols2.push(`"${entry.name}"`);
|
|
1549
|
+
localeVals.push(`$$${String(entry.values[code]).replace(/\$\$/g, "''")}$$`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (localeCols2.length > 0) {
|
|
1553
|
+
const insertCols = [
|
|
1554
|
+
`"${fkLocaleColName}"`,
|
|
1555
|
+
`"${fkColName}"`,
|
|
1556
|
+
...localeCols2,
|
|
1557
|
+
];
|
|
1558
|
+
const insertVals = [
|
|
1559
|
+
`(SELECT "id" FROM "locale" WHERE "code" = '${code}')`,
|
|
1560
|
+
parentSelect,
|
|
1561
|
+
...localeVals,
|
|
1562
|
+
];
|
|
1563
|
+
const setClauses2 = localeCols2
|
|
1564
|
+
.map((c) => `${c} = EXCLUDED.${c}`)
|
|
1565
|
+
.join(', ');
|
|
1566
|
+
sql.push(`INSERT INTO "${tableDef.name}_locale" (${insertCols.join(', ')}) VALUES (${insertVals.join(', ')}) ON CONFLICT ("${fkLocaleColName}", "${fkColName}") DO UPDATE SET ${setClauses2};`);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return sql;
|
|
1572
|
+
}
|
|
1573
|
+
resolveRawValue(value, colName, tableDef, allTables) {
|
|
1574
|
+
if (value === null || value === undefined)
|
|
1575
|
+
return 'NULL';
|
|
1576
|
+
if (typeof value === 'boolean')
|
|
1577
|
+
return value ? 'TRUE' : 'FALSE';
|
|
1578
|
+
if (typeof value === 'number')
|
|
1579
|
+
return String(value);
|
|
1580
|
+
if (typeof value === 'object' && 'where' in value) {
|
|
1581
|
+
const colDef = tableDef.columns.find((c) => c.name === colName);
|
|
1582
|
+
const refTableName = colDef?.references?.table;
|
|
1583
|
+
if (!refTableName) {
|
|
1584
|
+
console.warn(chalk.yellow(`[update] resolveRawValue: could not resolve FK table for column "${colName}" in "${tableDef.name}". Substituting NULL.`));
|
|
1585
|
+
return 'NULL';
|
|
1586
|
+
}
|
|
1587
|
+
const refTableDef = allTables.find((t) => t.name === refTableName);
|
|
1588
|
+
const refPk = refTableDef?.pk ?? 'id';
|
|
1589
|
+
const whereClause = this.getWhereString(value);
|
|
1590
|
+
return `(SELECT "${refPk}" FROM "${refTableName}" ${whereClause})`;
|
|
1591
|
+
}
|
|
1592
|
+
return `$$${String(value).replace(/\$\$/g, "''")}$$`;
|
|
1593
|
+
}
|
|
1594
|
+
async generateRelationUpdateSQL(parentTable, parentKeyColumn, parentKeyValue, targetTable, addedRelations, removedRelations, approvedRemovals, allTables, cwd) {
|
|
1595
|
+
const sql = [];
|
|
1596
|
+
const junctionResult = await this.tableService.getTableIntermediate(parentTable, targetTable, cwd);
|
|
1597
|
+
if (!junctionResult) {
|
|
1598
|
+
sql.push(`-- WARNING: Could not resolve junction table for relation "${parentTable}" ↔ "${targetTable}". Relation changes skipped.`, '');
|
|
1599
|
+
return sql;
|
|
1600
|
+
}
|
|
1601
|
+
const targetTableDef = allTables.find((t) => t.name === targetTable);
|
|
1602
|
+
const targetPk = targetTableDef?.pk ?? 'id';
|
|
1603
|
+
const parentTableDef = allTables.find((t) => t.name === parentTable);
|
|
1604
|
+
const parentPk = parentTableDef?.pk ?? 'id';
|
|
1605
|
+
const parentKeySQL = `(SELECT "${parentPk}" FROM "${parentTable}" WHERE "${parentKeyColumn}" = $$${String(parentKeyValue).replace(/\$\$/g, "''")}$$)`;
|
|
1606
|
+
if (junctionResult.intermediateTable) {
|
|
1607
|
+
const junctionName = junctionResult.name;
|
|
1608
|
+
const fkForParent = junctionResult.table.columns.find((c) => c.type === 'fk' && c.references?.table === parentTable)?.name;
|
|
1609
|
+
const fkForTarget = junctionResult.table.columns.find((c) => c.type === 'fk' && c.references?.table === targetTable)?.name;
|
|
1610
|
+
if (!fkForParent || !fkForTarget)
|
|
1611
|
+
return sql;
|
|
1612
|
+
for (const rel of addedRelations) {
|
|
1613
|
+
const whereClause = this.getWhereString(rel);
|
|
1614
|
+
const targetSQL = `(SELECT "${targetPk}" FROM "${targetTable}" ${whereClause})`;
|
|
1615
|
+
sql.push(`-- AddRelation: ${parentTable} ↔ ${targetTable}`, `INSERT INTO "${junctionName}" ("${fkForParent}", "${fkForTarget}") VALUES (${parentKeySQL}, ${targetSQL}) ON CONFLICT DO NOTHING;`, '');
|
|
1616
|
+
}
|
|
1617
|
+
for (const rel of removedRelations) {
|
|
1618
|
+
const isApproved = approvedRemovals.some((a) => a.parentKeyValue === parentKeyValue &&
|
|
1619
|
+
JSON.stringify(a.relation) === JSON.stringify(rel));
|
|
1620
|
+
if (!isApproved)
|
|
1621
|
+
continue;
|
|
1622
|
+
const whereClause = this.getWhereString(rel);
|
|
1623
|
+
const targetSQL = `(SELECT "${targetPk}" FROM "${targetTable}" ${whereClause})`;
|
|
1624
|
+
sql.push(`-- RemoveRelation: ${parentTable} ↔ ${targetTable}`, `DELETE FROM "${junctionName}" WHERE "${fkForParent}" = ${parentKeySQL} AND "${fkForTarget}" = ${targetSQL};`, '');
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
else {
|
|
1628
|
+
// Direct child relation
|
|
1629
|
+
const childTable = junctionResult.name;
|
|
1630
|
+
const fkForParent = junctionResult.table.columns.find((c) => c.type === 'fk' && c.references?.table === parentTable)?.name;
|
|
1631
|
+
if (!fkForParent)
|
|
1632
|
+
return sql;
|
|
1633
|
+
for (const rel of addedRelations) {
|
|
1634
|
+
const whereClause = this.getWhereString(rel);
|
|
1635
|
+
sql.push(`-- AddRelation (child): ${childTable}`, `INSERT INTO "${childTable}" ("${fkForParent}") SELECT ${parentKeySQL} WHERE EXISTS (SELECT 1 FROM "${targetTable}" ${whereClause}) ON CONFLICT DO NOTHING;`, '');
|
|
1636
|
+
}
|
|
1637
|
+
for (const rel of removedRelations) {
|
|
1638
|
+
const isApproved = approvedRemovals.some((a) => a.parentKeyValue === parentKeyValue &&
|
|
1639
|
+
JSON.stringify(a.relation) === JSON.stringify(rel));
|
|
1640
|
+
if (!isApproved)
|
|
1641
|
+
continue;
|
|
1642
|
+
const whereClause = this.getWhereString(rel);
|
|
1643
|
+
sql.push(`-- RemoveRelation (child): ${childTable}`, `DELETE FROM "${childTable}" WHERE "${fkForParent}" = ${parentKeySQL} AND EXISTS (SELECT 1 FROM "${targetTable}" ${whereClause});`, '');
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
return sql;
|
|
1647
|
+
}
|
|
1041
1648
|
};
|
|
1042
1649
|
exports.MigrationService = MigrationService;
|
|
1043
1650
|
exports.MigrationService = MigrationService = __decorate([
|
|
@@ -1045,6 +1652,7 @@ exports.MigrationService = MigrationService = __decorate([
|
|
|
1045
1652
|
__metadata("design:paramtypes", [database_service_1.DatabaseService,
|
|
1046
1653
|
developer_service_1.DeveloperService,
|
|
1047
1654
|
file_system_service_1.FileSystemService,
|
|
1048
|
-
table_service_1.TableService
|
|
1655
|
+
table_service_1.TableService,
|
|
1656
|
+
diff_service_1.DiffService])
|
|
1049
1657
|
], MigrationService);
|
|
1050
1658
|
//# sourceMappingURL=migration.service.js.map
|