@hed-hog/cli 0.0.95 → 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/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/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 +35 -1
- package/dist/src/modules/hedhog/services/migration.service.js +601 -7
- 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 +74 -0
- 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) {
|
|
@@ -311,6 +314,64 @@ let MigrationService = class MigrationService {
|
|
|
311
314
|
}
|
|
312
315
|
return transformedContent;
|
|
313
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
|
+
}
|
|
314
375
|
normalizeCustomSqlQuery(file, sqlContent, enumRegistry, columnRegistry) {
|
|
315
376
|
const normalizedDeclarations = this.normalizeCustomSqlEnumDeclarations(file, sqlContent, enumRegistry, columnRegistry);
|
|
316
377
|
return this.normalizeCustomSqlAgainstSchema(file, normalizedDeclarations, columnRegistry);
|
|
@@ -574,12 +635,12 @@ let MigrationService = class MigrationService {
|
|
|
574
635
|
this.log(`Creating unique constraints for table: ${table.name}`);
|
|
575
636
|
const uniqueColumns = table.columns.filter((c) => c.isUnique);
|
|
576
637
|
if (uniqueColumns.length) {
|
|
577
|
-
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(', ')});`, ``);
|
|
578
639
|
}
|
|
579
640
|
for (const column of table.columns) {
|
|
580
641
|
if (column.type === 'slug') {
|
|
581
642
|
this.log(`Adding unique constraint for column: ${column.name}`);
|
|
582
|
-
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}");`, ``);
|
|
583
644
|
}
|
|
584
645
|
}
|
|
585
646
|
}
|
|
@@ -844,9 +905,9 @@ let MigrationService = class MigrationService {
|
|
|
844
905
|
}
|
|
845
906
|
return [];
|
|
846
907
|
}
|
|
847
|
-
async afterQueries(name, cwd, migrationContent, enumRegistry, columnRegistry) {
|
|
908
|
+
async afterQueries(name, cwd, migrationContent, enumRegistry, columnRegistry, customQueriesPath) {
|
|
848
909
|
this.log(`Running after queries for ${name}...`);
|
|
849
|
-
const queriesPath = (0, path_1.resolve)(cwd, `./libraries/${name}/hedhog/query`);
|
|
910
|
+
const queriesPath = customQueriesPath ?? (0, path_1.resolve)(cwd, `./libraries/${name}/hedhog/query`);
|
|
850
911
|
try {
|
|
851
912
|
if (await this.fileSystem.exists(queriesPath)) {
|
|
852
913
|
const allFiles = await this.fileSystem.listFiles(queriesPath, () => true);
|
|
@@ -854,7 +915,7 @@ let MigrationService = class MigrationService {
|
|
|
854
915
|
if (file.endsWith('.sql')) {
|
|
855
916
|
const sqlContent = await this.fileSystem.readFileContent((0, path_1.join)(queriesPath, file));
|
|
856
917
|
const normalizedSql = this.normalizeCustomSqlQuery(file, sqlContent, enumRegistry, columnRegistry);
|
|
857
|
-
migrationContent.push(`-- AfterQuery: ${file}`, normalizedSql, '');
|
|
918
|
+
migrationContent.push(`-- AfterQuery: ${file}`, this.wrapIdempotentSql(normalizedSql), '');
|
|
858
919
|
}
|
|
859
920
|
else {
|
|
860
921
|
console.warn(chalk.yellow(`Arquivo ignorado no diretório de queries: ${file}`));
|
|
@@ -1052,6 +1113,538 @@ let MigrationService = class MigrationService {
|
|
|
1052
1113
|
await this.fileSystem.writeJsonFile(hedhogFilePath, hedhogFile);
|
|
1053
1114
|
this.log(`Migration file ${migrationFileName} created successfully.`);
|
|
1054
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
|
+
}
|
|
1055
1648
|
};
|
|
1056
1649
|
exports.MigrationService = MigrationService;
|
|
1057
1650
|
exports.MigrationService = MigrationService = __decorate([
|
|
@@ -1059,6 +1652,7 @@ exports.MigrationService = MigrationService = __decorate([
|
|
|
1059
1652
|
__metadata("design:paramtypes", [database_service_1.DatabaseService,
|
|
1060
1653
|
developer_service_1.DeveloperService,
|
|
1061
1654
|
file_system_service_1.FileSystemService,
|
|
1062
|
-
table_service_1.TableService
|
|
1655
|
+
table_service_1.TableService,
|
|
1656
|
+
diff_service_1.DiffService])
|
|
1063
1657
|
], MigrationService);
|
|
1064
1658
|
//# sourceMappingURL=migration.service.js.map
|