@javalabs/prisma-client 1.0.27 → 1.0.29
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/.github/CODEOWNERS +1 -1
- package/README.md +269 -269
- package/migration-config.json +63 -63
- package/migration-config.json.bk +95 -95
- package/migrations/add_reserved_amount.sql +7 -7
- package/package.json +44 -44
- package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
- package/prisma/schema.prisma +609 -601
- package/src/index.ts +23 -23
- package/src/prisma-factory.service.ts +40 -40
- package/src/prisma.module.ts +9 -9
- package/src/prisma.service.ts +16 -16
- package/src/scripts/add-uuid-to-table.ts +138 -138
- package/src/scripts/create-tenant-schemas.ts +145 -145
- package/src/scripts/data-migration/batch-migrator.ts +248 -248
- package/src/scripts/data-migration/data-transformer.ts +426 -426
- package/src/scripts/data-migration/db-connector.ts +120 -120
- package/src/scripts/data-migration/dependency-resolver.ts +174 -174
- package/src/scripts/data-migration/entity-discovery.ts +196 -196
- package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
- package/src/scripts/data-migration/migration-config.json +63 -63
- package/src/scripts/data-migration/migration-tool.ts +509 -509
- package/src/scripts/data-migration/schema-utils.ts +248 -248
- package/src/scripts/data-migration/tenant-migrator.ts +201 -201
- package/src/scripts/data-migration/typecast-manager.ts +193 -193
- package/src/scripts/data-migration/types.ts +113 -113
- package/src/scripts/database-initializer.ts +49 -49
- package/src/scripts/drop-database.ts +104 -104
- package/src/scripts/dump-source-db.sh +61 -61
- package/src/scripts/encrypt-user-passwords.ts +36 -36
- package/src/scripts/error-handler.ts +117 -117
- package/src/scripts/fix-data-types.ts +241 -241
- package/src/scripts/fix-enum-values.ts +357 -357
- package/src/scripts/fix-schema-discrepancies.ts +317 -317
- package/src/scripts/fix-table-indexes.ts +601 -601
- package/src/scripts/migrate-schema-structure.ts +90 -90
- package/src/scripts/migrate-uuid.ts +76 -76
- package/src/scripts/post-migration-validator.ts +526 -526
- package/src/scripts/pre-migration-validator.ts +610 -610
- package/src/scripts/reset-database.ts +263 -263
- package/src/scripts/retry-failed-migrations.ts +416 -416
- package/src/scripts/run-migration.ts +707 -707
- package/src/scripts/schema-sync.ts +128 -128
- package/src/scripts/sequence-sync-cli.ts +416 -416
- package/src/scripts/sequence-synchronizer.ts +127 -127
- package/src/scripts/sync-enum-types.ts +170 -170
- package/src/scripts/sync-enum-values.ts +563 -563
- package/src/scripts/truncate-database.ts +123 -123
- package/src/scripts/verify-migration-setup.ts +135 -135
- package/tsconfig.json +17 -17
|
@@ -1,611 +1,611 @@
|
|
|
1
|
-
import * as pg from 'pg';
|
|
2
|
-
import * as dotenv from 'dotenv';
|
|
3
|
-
import { Logger } from '@nestjs/common';
|
|
4
|
-
import * as fs from 'fs';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
|
|
7
|
-
dotenv.config();
|
|
8
|
-
|
|
9
|
-
export class PreMigrationValidator {
|
|
10
|
-
private readonly logger = new Logger('PreMigrationValidator');
|
|
11
|
-
private readonly sourcePool: pg.Pool;
|
|
12
|
-
private readonly targetPool: pg.Pool;
|
|
13
|
-
private readonly reportPath: string;
|
|
14
|
-
private issues: any[] = [];
|
|
15
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
|
|
18
|
-
private readonly targetUrl: string = process.env.DATABASE_URL
|
|
19
|
-
) {
|
|
20
|
-
this.sourcePool = new pg.Pool({
|
|
21
|
-
connectionString: this.sourceUrl,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
this.targetPool = new pg.Pool({
|
|
25
|
-
connectionString: this.targetUrl,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Crear directorio para reportes si no existe
|
|
29
|
-
const reportsDir = path.join(process.cwd(), 'migration-logs');
|
|
30
|
-
if (!fs.existsSync(reportsDir)) {
|
|
31
|
-
fs.mkdirSync(reportsDir, { recursive: true });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Archivo para guardar el reporte
|
|
35
|
-
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
36
|
-
this.reportPath = path.join(reportsDir, `pre-migration-report-${timestamp}.json`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async validate() {
|
|
40
|
-
try {
|
|
41
|
-
this.logger.log('Starting pre-migration validation');
|
|
42
|
-
|
|
43
|
-
// 1. Verificar conexiones a las bases de datos
|
|
44
|
-
await this.validateDatabaseConnections();
|
|
45
|
-
|
|
46
|
-
// 2. Verificar API keys en la base de datos de origen
|
|
47
|
-
await this.validateApiKeys();
|
|
48
|
-
|
|
49
|
-
// 3. Verificar compatibilidad de tipos de datos
|
|
50
|
-
await this.validateDataTypeCompatibility();
|
|
51
|
-
|
|
52
|
-
// 4. Verificar tamaños de columnas
|
|
53
|
-
await this.validateColumnSizes();
|
|
54
|
-
|
|
55
|
-
// 5. Verificar valores de enum
|
|
56
|
-
await this.validateEnumValues();
|
|
57
|
-
|
|
58
|
-
// 6. Verificar restricciones de clave foránea
|
|
59
|
-
await this.validateForeignKeyConstraints();
|
|
60
|
-
|
|
61
|
-
// 7. Verificar índices únicos
|
|
62
|
-
await this.validateUniqueConstraints();
|
|
63
|
-
|
|
64
|
-
// Guardar el reporte
|
|
65
|
-
this.saveReport();
|
|
66
|
-
|
|
67
|
-
// Mostrar resumen
|
|
68
|
-
this.logger.log(`Validation completed with ${this.issues.length} issues found`);
|
|
69
|
-
if (this.issues.length > 0) {
|
|
70
|
-
this.logger.log(`Check the full report at: ${this.reportPath}`);
|
|
71
|
-
|
|
72
|
-
// Mostrar los primeros 5 problemas como ejemplo
|
|
73
|
-
this.logger.log('Sample issues:');
|
|
74
|
-
for (let i = 0; i < Math.min(5, this.issues.length); i++) {
|
|
75
|
-
const issue = this.issues[i];
|
|
76
|
-
this.logger.log(`- ${issue.type}: ${issue.message}`);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
this.logger.log('No issues found. Migration should proceed smoothly.');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
success: this.issues.length === 0,
|
|
84
|
-
issueCount: this.issues.length,
|
|
85
|
-
reportPath: this.reportPath
|
|
86
|
-
};
|
|
87
|
-
} catch (error) {
|
|
88
|
-
this.logger.error(`Error during validation: ${error.message}`, error.stack);
|
|
89
|
-
this.issues.push({
|
|
90
|
-
type: 'VALIDATION_ERROR',
|
|
91
|
-
message: `Validation process failed: ${error.message}`,
|
|
92
|
-
details: error.stack
|
|
93
|
-
});
|
|
94
|
-
this.saveReport();
|
|
95
|
-
return {
|
|
96
|
-
success: false,
|
|
97
|
-
issueCount: this.issues.length,
|
|
98
|
-
reportPath: this.reportPath
|
|
99
|
-
};
|
|
100
|
-
} finally {
|
|
101
|
-
await this.cleanup();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
private async validateDatabaseConnections() {
|
|
106
|
-
this.logger.log('Validating database connections');
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
// Verificar conexión a la base de datos de origen
|
|
110
|
-
await this.sourcePool.query('SELECT 1');
|
|
111
|
-
this.logger.log('Source database connection successful');
|
|
112
|
-
} catch (error) {
|
|
113
|
-
this.logger.error(`Source database connection failed: ${error.message}`);
|
|
114
|
-
this.issues.push({
|
|
115
|
-
type: 'CONNECTION_ERROR',
|
|
116
|
-
source: 'source',
|
|
117
|
-
message: `Cannot connect to source database: ${error.message}`
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
// Verificar conexión a la base de datos de destino
|
|
123
|
-
await this.targetPool.query('SELECT 1');
|
|
124
|
-
this.logger.log('Target database connection successful');
|
|
125
|
-
} catch (error) {
|
|
126
|
-
this.logger.error(`Target database connection failed: ${error.message}`);
|
|
127
|
-
this.issues.push({
|
|
128
|
-
type: 'CONNECTION_ERROR',
|
|
129
|
-
source: 'target',
|
|
130
|
-
message: `Cannot connect to target database: ${error.message}`
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async validateApiKeys() {
|
|
136
|
-
this.logger.log('Validating API keys');
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
// Verificar que existan API keys en la base de datos de origen
|
|
140
|
-
const result = await this.sourcePool.query(`
|
|
141
|
-
SELECT COUNT(*) as count FROM api_keys
|
|
142
|
-
`);
|
|
143
|
-
|
|
144
|
-
const count = parseInt(result.rows[0].count);
|
|
145
|
-
|
|
146
|
-
if (count === 0) {
|
|
147
|
-
this.logger.warn('No API keys found in source database');
|
|
148
|
-
this.issues.push({
|
|
149
|
-
type: 'DATA_ERROR',
|
|
150
|
-
message: 'No API keys found in source database. Migration will not create any tenant schemas.'
|
|
151
|
-
});
|
|
152
|
-
} else {
|
|
153
|
-
this.logger.log(`Found ${count} API keys in source database`);
|
|
154
|
-
}
|
|
155
|
-
} catch (error) {
|
|
156
|
-
this.logger.error(`Error validating API keys: ${error.message}`);
|
|
157
|
-
this.issues.push({
|
|
158
|
-
type: 'VALIDATION_ERROR',
|
|
159
|
-
message: `Error validating API keys: ${error.message}`
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private async validateDataTypeCompatibility() {
|
|
165
|
-
this.logger.log('Validating data type compatibility');
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
// Obtener todas las tablas de la base de datos de origen
|
|
169
|
-
const tablesResult = await this.sourcePool.query(`
|
|
170
|
-
SELECT table_name
|
|
171
|
-
FROM information_schema.tables
|
|
172
|
-
WHERE table_schema = 'public'
|
|
173
|
-
AND table_type = 'BASE TABLE'
|
|
174
|
-
`);
|
|
175
|
-
|
|
176
|
-
for (const tableRow of tablesResult.rows) {
|
|
177
|
-
const tableName = tableRow.table_name;
|
|
178
|
-
|
|
179
|
-
// Obtener columnas de la tabla en la base de datos de origen
|
|
180
|
-
const sourceColumnsResult = await this.sourcePool.query(`
|
|
181
|
-
SELECT column_name, data_type, udt_name
|
|
182
|
-
FROM information_schema.columns
|
|
183
|
-
WHERE table_schema = 'public'
|
|
184
|
-
AND table_name = $1
|
|
185
|
-
`, [tableName]);
|
|
186
|
-
|
|
187
|
-
// Verificar si la tabla existe en la base de datos de destino (schema public)
|
|
188
|
-
const targetTableExists = await this.targetPool.query(`
|
|
189
|
-
SELECT 1
|
|
190
|
-
FROM information_schema.tables
|
|
191
|
-
WHERE table_schema = 'public'
|
|
192
|
-
AND table_name = $1
|
|
193
|
-
LIMIT 1
|
|
194
|
-
`, [tableName]);
|
|
195
|
-
|
|
196
|
-
if (targetTableExists.rows.length === 0) {
|
|
197
|
-
// Si la tabla no existe en el destino, no hay problema de compatibilidad
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Obtener columnas de la tabla en la base de datos de destino
|
|
202
|
-
const targetColumnsResult = await this.targetPool.query(`
|
|
203
|
-
SELECT column_name, data_type, udt_name
|
|
204
|
-
FROM information_schema.columns
|
|
205
|
-
WHERE table_schema = 'public'
|
|
206
|
-
AND table_name = $1
|
|
207
|
-
`, [tableName]);
|
|
208
|
-
|
|
209
|
-
// Crear mapas para facilitar la comparación
|
|
210
|
-
const sourceColumns = sourceColumnsResult.rows.reduce((map, col) => {
|
|
211
|
-
map[col.column_name] = col;
|
|
212
|
-
return map;
|
|
213
|
-
}, {});
|
|
214
|
-
|
|
215
|
-
const targetColumns = targetColumnsResult.rows.reduce((map, col) => {
|
|
216
|
-
map[col.column_name] = col;
|
|
217
|
-
return map;
|
|
218
|
-
}, {});
|
|
219
|
-
|
|
220
|
-
// Comparar tipos de datos
|
|
221
|
-
for (const columnName in sourceColumns) {
|
|
222
|
-
if (targetColumns[columnName]) {
|
|
223
|
-
const sourceType = sourceColumns[columnName].data_type;
|
|
224
|
-
const targetType = targetColumns[columnName].data_type;
|
|
225
|
-
|
|
226
|
-
// Verificar si los tipos son compatibles
|
|
227
|
-
if (sourceType !== targetType) {
|
|
228
|
-
// Algunos tipos pueden ser compatibles aunque tengan nombres diferentes
|
|
229
|
-
const isCompatible = this.areTypesCompatible(sourceType, targetType);
|
|
230
|
-
|
|
231
|
-
if (!isCompatible) {
|
|
232
|
-
this.logger.warn(`Data type mismatch for ${tableName}.${columnName}: ${sourceType} (source) vs ${targetType} (target)`);
|
|
233
|
-
this.issues.push({
|
|
234
|
-
type: 'TYPE_MISMATCH',
|
|
235
|
-
table: tableName,
|
|
236
|
-
column: columnName,
|
|
237
|
-
sourceType,
|
|
238
|
-
targetType,
|
|
239
|
-
message: `Data type mismatch for ${tableName}.${columnName}: ${sourceType} (source) vs ${targetType} (target)`
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
} catch (error) {
|
|
247
|
-
this.logger.error(`Error validating data type compatibility: ${error.message}`);
|
|
248
|
-
this.issues.push({
|
|
249
|
-
type: 'VALIDATION_ERROR',
|
|
250
|
-
message: `Error validating data type compatibility: ${error.message}`
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
private areTypesCompatible(sourceType: string, targetType: string): boolean {
|
|
256
|
-
// Definir pares de tipos compatibles
|
|
257
|
-
const compatibleTypes = [
|
|
258
|
-
['character varying', 'varchar'],
|
|
259
|
-
['character', 'char'],
|
|
260
|
-
['integer', 'int'],
|
|
261
|
-
['boolean', 'bool'],
|
|
262
|
-
['double precision', 'float8'],
|
|
263
|
-
['real', 'float4'],
|
|
264
|
-
['timestamp without time zone', 'timestamp'],
|
|
265
|
-
['timestamp with time zone', 'timestamptz']
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
// Verificar si los tipos son iguales
|
|
269
|
-
if (sourceType === targetType) {
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Verificar si los tipos son compatibles
|
|
274
|
-
return compatibleTypes.some(pair =>
|
|
275
|
-
(pair[0] === sourceType && pair[1] === targetType) ||
|
|
276
|
-
(pair[0] === targetType && pair[1] === sourceType)
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async validateColumnSizes() {
|
|
281
|
-
this.logger.log('Validating column sizes');
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
// Obtener todas las tablas de la base de datos de origen
|
|
285
|
-
const tablesResult = await this.sourcePool.query(`
|
|
286
|
-
SELECT table_name
|
|
287
|
-
FROM information_schema.tables
|
|
288
|
-
WHERE table_schema = 'public'
|
|
289
|
-
AND table_type = 'BASE TABLE'
|
|
290
|
-
`);
|
|
291
|
-
|
|
292
|
-
for (const tableRow of tablesResult.rows) {
|
|
293
|
-
const tableName = tableRow.table_name;
|
|
294
|
-
|
|
295
|
-
// Obtener columnas de tipo character con longitud máxima
|
|
296
|
-
const sourceColumnsResult = await this.sourcePool.query(`
|
|
297
|
-
SELECT column_name, data_type, character_maximum_length
|
|
298
|
-
FROM information_schema.columns
|
|
299
|
-
WHERE table_schema = 'public'
|
|
300
|
-
AND table_name = $1
|
|
301
|
-
AND data_type IN ('character varying', 'character', 'varchar', 'char')
|
|
302
|
-
AND character_maximum_length IS NOT NULL
|
|
303
|
-
`, [tableName]);
|
|
304
|
-
|
|
305
|
-
// Verificar si la tabla existe en la base de datos de destino (schema public)
|
|
306
|
-
const targetTableExists = await this.targetPool.query(`
|
|
307
|
-
SELECT 1
|
|
308
|
-
FROM information_schema.tables
|
|
309
|
-
WHERE table_schema = 'public'
|
|
310
|
-
AND table_name = $1
|
|
311
|
-
LIMIT 1
|
|
312
|
-
`, [tableName]);
|
|
313
|
-
|
|
314
|
-
if (targetTableExists.rows.length === 0) {
|
|
315
|
-
// Si la tabla no existe en el destino, no hay problema de tamaño
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Para cada columna, verificar si el tamaño en el destino es suficiente
|
|
320
|
-
for (const sourceColumn of sourceColumnsResult.rows) {
|
|
321
|
-
const columnName = sourceColumn.column_name;
|
|
322
|
-
const sourceMaxLength = sourceColumn.character_maximum_length;
|
|
323
|
-
|
|
324
|
-
// Obtener la columna correspondiente en el destino
|
|
325
|
-
const targetColumnResult = await this.targetPool.query(`
|
|
326
|
-
SELECT character_maximum_length
|
|
327
|
-
FROM information_schema.columns
|
|
328
|
-
WHERE table_schema = 'public'
|
|
329
|
-
AND table_name = $1
|
|
330
|
-
AND column_name = $2
|
|
331
|
-
AND data_type IN ('character varying', 'character', 'varchar', 'char')
|
|
332
|
-
`, [tableName, columnName]);
|
|
333
|
-
|
|
334
|
-
if (targetColumnResult.rows.length > 0) {
|
|
335
|
-
const targetMaxLength = targetColumnResult.rows[0].character_maximum_length;
|
|
336
|
-
|
|
337
|
-
// Si el tamaño en el destino es menor, podría haber truncamiento
|
|
338
|
-
if (targetMaxLength < sourceMaxLength) {
|
|
339
|
-
this.logger.warn(`Column size mismatch for ${tableName}.${columnName}: ${sourceMaxLength} (source) vs ${targetMaxLength} (target)`);
|
|
340
|
-
this.issues.push({
|
|
341
|
-
type: 'SIZE_MISMATCH',
|
|
342
|
-
table: tableName,
|
|
343
|
-
column: columnName,
|
|
344
|
-
sourceSize: sourceMaxLength,
|
|
345
|
-
targetSize: targetMaxLength,
|
|
346
|
-
message: `Column size mismatch for ${tableName}.${columnName}: ${sourceMaxLength} (source) vs ${targetMaxLength} (target). Data may be truncated.`
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
} catch (error) {
|
|
353
|
-
this.logger.error(`Error validating column sizes: ${error.message}`);
|
|
354
|
-
this.issues.push({
|
|
355
|
-
type: 'VALIDATION_ERROR',
|
|
356
|
-
message: `Error validating column sizes: ${error.message}`
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private async validateEnumValues() {
|
|
362
|
-
this.logger.log('Validating enum values');
|
|
363
|
-
|
|
364
|
-
try {
|
|
365
|
-
// Obtener todos los tipos enum en la base de datos de origen
|
|
366
|
-
const sourceEnumsResult = await this.sourcePool.query(`
|
|
367
|
-
SELECT t.typname AS enum_name
|
|
368
|
-
FROM pg_type t
|
|
369
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
370
|
-
WHERE t.typtype = 'e'
|
|
371
|
-
AND n.nspname = 'public'
|
|
372
|
-
`);
|
|
373
|
-
|
|
374
|
-
// Para cada tipo enum, obtener sus valores
|
|
375
|
-
for (const enumRow of sourceEnumsResult.rows) {
|
|
376
|
-
const enumName = enumRow.enum_name;
|
|
377
|
-
|
|
378
|
-
// Obtener valores del enum en la base de datos de origen
|
|
379
|
-
const sourceEnumValuesResult = await this.sourcePool.query(`
|
|
380
|
-
SELECT e.enumlabel
|
|
381
|
-
FROM pg_enum e
|
|
382
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
383
|
-
WHERE t.typname = $1
|
|
384
|
-
ORDER BY e.enumsortorder
|
|
385
|
-
`, [enumName]);
|
|
386
|
-
|
|
387
|
-
const sourceEnumValues = sourceEnumValuesResult.rows.map(row => row.enumlabel);
|
|
388
|
-
|
|
389
|
-
// Verificar si el enum existe en la base de datos de destino
|
|
390
|
-
const targetEnumExistsResult = await this.targetPool.query(`
|
|
391
|
-
SELECT 1
|
|
392
|
-
FROM pg_type t
|
|
393
|
-
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
394
|
-
WHERE t.typtype = 'e'
|
|
395
|
-
AND t.typname = $1
|
|
396
|
-
AND n.nspname = 'public'
|
|
397
|
-
LIMIT 1
|
|
398
|
-
`, [enumName]);
|
|
399
|
-
|
|
400
|
-
if (targetEnumExistsResult.rows.length === 0) {
|
|
401
|
-
// Si el enum no existe en el destino, no hay problema de compatibilidad
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Obtener valores del enum en la base de datos de destino
|
|
406
|
-
const targetEnumValuesResult = await this.targetPool.query(`
|
|
407
|
-
SELECT e.enumlabel
|
|
408
|
-
FROM pg_enum e
|
|
409
|
-
JOIN pg_type t ON e.enumtypid = t.oid
|
|
410
|
-
WHERE t.typname = $1
|
|
411
|
-
ORDER BY e.enumsortorder
|
|
412
|
-
`, [enumName]);
|
|
413
|
-
|
|
414
|
-
const targetEnumValues = targetEnumValuesResult.rows.map(row => row.enumlabel);
|
|
415
|
-
|
|
416
|
-
// Verificar si todos los valores del origen existen en el destino
|
|
417
|
-
for (const sourceValue of sourceEnumValues) {
|
|
418
|
-
if (!targetEnumValues.includes(sourceValue)) {
|
|
419
|
-
this.logger.warn(`Enum value '${sourceValue}' for type ${enumName} exists in source but not in target`);
|
|
420
|
-
this.issues.push({
|
|
421
|
-
type: 'ENUM_VALUE_MISSING',
|
|
422
|
-
enumType: enumName,
|
|
423
|
-
value: sourceValue,
|
|
424
|
-
message: `Enum value '${sourceValue}' for type ${enumName} exists in source but not in target. Data migration will fail for records with this value.`
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch (error) {
|
|
430
|
-
this.logger.error(`Error validating enum values: ${error.message}`);
|
|
431
|
-
this.issues.push({
|
|
432
|
-
type: 'VALIDATION_ERROR',
|
|
433
|
-
message: `Error validating enum values: ${error.message}`
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
private async validateForeignKeyConstraints() {
|
|
439
|
-
this.logger.log('Validating foreign key constraints');
|
|
440
|
-
|
|
441
|
-
// Esta validación es más compleja y depende de la estructura específica
|
|
442
|
-
// Por ahora, solo verificamos si hay tablas con claves foráneas en el origen
|
|
443
|
-
try {
|
|
444
|
-
const foreignKeysResult = await this.sourcePool.query(`
|
|
445
|
-
SELECT
|
|
446
|
-
tc.table_name,
|
|
447
|
-
kcu.column_name,
|
|
448
|
-
ccu.table_name AS foreign_table_name,
|
|
449
|
-
ccu.column_name AS foreign_column_name
|
|
450
|
-
FROM
|
|
451
|
-
information_schema.table_constraints AS tc
|
|
452
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
453
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
454
|
-
AND tc.table_schema = kcu.table_schema
|
|
455
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
456
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
457
|
-
AND ccu.table_schema = tc.table_schema
|
|
458
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
459
|
-
AND tc.table_schema = 'public'
|
|
460
|
-
`);
|
|
461
|
-
|
|
462
|
-
this.logger.log(`Found ${foreignKeysResult.rows.length} foreign key constraints in source database`);
|
|
463
|
-
|
|
464
|
-
// Aquí podríamos hacer validaciones más específicas si es necesario
|
|
465
|
-
} catch (error) {
|
|
466
|
-
this.logger.error(`Error validating foreign key constraints: ${error.message}`);
|
|
467
|
-
this.issues.push({
|
|
468
|
-
type: 'VALIDATION_ERROR',
|
|
469
|
-
message: `Error validating foreign key constraints: ${error.message}`
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private async validateUniqueConstraints() {
|
|
475
|
-
this.logger.log('Validating unique constraints...');
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
// Obtener restricciones únicas de la base de datos de origen
|
|
479
|
-
const sourceConstraintsQuery = `
|
|
480
|
-
SELECT
|
|
481
|
-
tc.table_name,
|
|
482
|
-
kcu.column_name
|
|
483
|
-
FROM
|
|
484
|
-
information_schema.table_constraints tc
|
|
485
|
-
JOIN information_schema.key_column_usage kcu
|
|
486
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
487
|
-
WHERE
|
|
488
|
-
tc.constraint_type = 'UNIQUE'
|
|
489
|
-
AND tc.table_schema = 'public'
|
|
490
|
-
`;
|
|
491
|
-
|
|
492
|
-
const sourceConstraints = await this.sourcePool.query(sourceConstraintsQuery);
|
|
493
|
-
|
|
494
|
-
// Para cada restricción, verificar si existe en la base de datos de destino
|
|
495
|
-
for (const constraint of sourceConstraints.rows) {
|
|
496
|
-
const { table_name, column_name } = constraint;
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
// Verificar si la columna existe en la tabla de destino
|
|
500
|
-
const columnExistsQuery = `
|
|
501
|
-
SELECT column_name
|
|
502
|
-
FROM information_schema.columns
|
|
503
|
-
WHERE table_name = $1
|
|
504
|
-
AND column_name = $2
|
|
505
|
-
AND table_schema = 'public'
|
|
506
|
-
`;
|
|
507
|
-
|
|
508
|
-
const columnExists = await this.targetPool.query(columnExistsQuery, [table_name, column_name]);
|
|
509
|
-
|
|
510
|
-
if (columnExists.rows.length === 0) {
|
|
511
|
-
// La columna no existe, registrar como advertencia pero no como error crítico
|
|
512
|
-
this.issues.push({
|
|
513
|
-
type: 'COLUMN_MISMATCH',
|
|
514
|
-
message: `Column "${column_name}" in table "${table_name}" exists in source but not in target`,
|
|
515
|
-
severity: 'WARNING',
|
|
516
|
-
details: {
|
|
517
|
-
table: table_name,
|
|
518
|
-
column: column_name,
|
|
519
|
-
constraint_type: 'UNIQUE'
|
|
520
|
-
}
|
|
521
|
-
});
|
|
522
|
-
continue; // Saltar a la siguiente restricción
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Resto del código para validar restricciones únicas...
|
|
526
|
-
} catch (error) {
|
|
527
|
-
// Capturar errores específicos de columnas que no existen
|
|
528
|
-
if (error.message.includes('does not exist')) {
|
|
529
|
-
this.issues.push({
|
|
530
|
-
type: 'COLUMN_MISMATCH',
|
|
531
|
-
message: `Error checking column "${column_name}" in table "${table_name}": ${error.message}`,
|
|
532
|
-
severity: 'WARNING',
|
|
533
|
-
details: {
|
|
534
|
-
table: table_name,
|
|
535
|
-
column: column_name,
|
|
536
|
-
error: error.message
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
} else {
|
|
540
|
-
throw error; // Re-lanzar otros errores
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
this.logger.log('Unique constraints validation completed');
|
|
546
|
-
} catch (error) {
|
|
547
|
-
// Convertir errores críticos en advertencias para permitir que la migración continúe
|
|
548
|
-
this.issues.push({
|
|
549
|
-
type: 'VALIDATION_ERROR',
|
|
550
|
-
message: `Error validating unique constraints: ${error.message}`,
|
|
551
|
-
severity: 'WARNING',
|
|
552
|
-
details: {
|
|
553
|
-
error: error.message,
|
|
554
|
-
stack: error.stack
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
this.logger.warn(`Error during unique constraints validation: ${error.message}`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
private saveReport() {
|
|
563
|
-
try {
|
|
564
|
-
const report = {
|
|
565
|
-
timestamp: new Date().toISOString(),
|
|
566
|
-
sourceDatabase: this.sourceUrl,
|
|
567
|
-
targetDatabase: this.targetUrl,
|
|
568
|
-
issueCount: this.issues.length,
|
|
569
|
-
issues: this.issues
|
|
570
|
-
};
|
|
571
|
-
|
|
572
|
-
fs.writeFileSync(
|
|
573
|
-
this.reportPath,
|
|
574
|
-
JSON.stringify(report, null, 2),
|
|
575
|
-
'utf8'
|
|
576
|
-
);
|
|
577
|
-
} catch (error) {
|
|
578
|
-
this.logger.error(`Error saving validation report: ${error.message}`);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
private async cleanup() {
|
|
583
|
-
this.logger.log('Cleaning up database connections');
|
|
584
|
-
await this.sourcePool.end();
|
|
585
|
-
await this.targetPool.end();
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Script para ejecutar desde línea de comandos
|
|
590
|
-
if (require.main === module) {
|
|
591
|
-
const run = async () => {
|
|
592
|
-
try {
|
|
593
|
-
const validator = new PreMigrationValidator();
|
|
594
|
-
const result = await validator.validate();
|
|
595
|
-
|
|
596
|
-
if (result.success) {
|
|
597
|
-
console.log('Validation successful! No issues found.');
|
|
598
|
-
process.exit(0);
|
|
599
|
-
} else {
|
|
600
|
-
console.log(`Validation completed with ${result.issueCount} issues found.`);
|
|
601
|
-
console.log(`Check the full report at: ${result.reportPath}`);
|
|
602
|
-
process.exit(1);
|
|
603
|
-
}
|
|
604
|
-
} catch (error) {
|
|
605
|
-
console.error('Error:', error.message);
|
|
606
|
-
process.exit(1);
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
run();
|
|
1
|
+
import * as pg from 'pg';
|
|
2
|
+
import * as dotenv from 'dotenv';
|
|
3
|
+
import { Logger } from '@nestjs/common';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
dotenv.config();
|
|
8
|
+
|
|
9
|
+
export class PreMigrationValidator {
|
|
10
|
+
private readonly logger = new Logger('PreMigrationValidator');
|
|
11
|
+
private readonly sourcePool: pg.Pool;
|
|
12
|
+
private readonly targetPool: pg.Pool;
|
|
13
|
+
private readonly reportPath: string;
|
|
14
|
+
private issues: any[] = [];
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly sourceUrl: string = process.env.SOURCE_DATABASE_URL,
|
|
18
|
+
private readonly targetUrl: string = process.env.DATABASE_URL
|
|
19
|
+
) {
|
|
20
|
+
this.sourcePool = new pg.Pool({
|
|
21
|
+
connectionString: this.sourceUrl,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
this.targetPool = new pg.Pool({
|
|
25
|
+
connectionString: this.targetUrl,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Crear directorio para reportes si no existe
|
|
29
|
+
const reportsDir = path.join(process.cwd(), 'migration-logs');
|
|
30
|
+
if (!fs.existsSync(reportsDir)) {
|
|
31
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Archivo para guardar el reporte
|
|
35
|
+
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
|
36
|
+
this.reportPath = path.join(reportsDir, `pre-migration-report-${timestamp}.json`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async validate() {
|
|
40
|
+
try {
|
|
41
|
+
this.logger.log('Starting pre-migration validation');
|
|
42
|
+
|
|
43
|
+
// 1. Verificar conexiones a las bases de datos
|
|
44
|
+
await this.validateDatabaseConnections();
|
|
45
|
+
|
|
46
|
+
// 2. Verificar API keys en la base de datos de origen
|
|
47
|
+
await this.validateApiKeys();
|
|
48
|
+
|
|
49
|
+
// 3. Verificar compatibilidad de tipos de datos
|
|
50
|
+
await this.validateDataTypeCompatibility();
|
|
51
|
+
|
|
52
|
+
// 4. Verificar tamaños de columnas
|
|
53
|
+
await this.validateColumnSizes();
|
|
54
|
+
|
|
55
|
+
// 5. Verificar valores de enum
|
|
56
|
+
await this.validateEnumValues();
|
|
57
|
+
|
|
58
|
+
// 6. Verificar restricciones de clave foránea
|
|
59
|
+
await this.validateForeignKeyConstraints();
|
|
60
|
+
|
|
61
|
+
// 7. Verificar índices únicos
|
|
62
|
+
await this.validateUniqueConstraints();
|
|
63
|
+
|
|
64
|
+
// Guardar el reporte
|
|
65
|
+
this.saveReport();
|
|
66
|
+
|
|
67
|
+
// Mostrar resumen
|
|
68
|
+
this.logger.log(`Validation completed with ${this.issues.length} issues found`);
|
|
69
|
+
if (this.issues.length > 0) {
|
|
70
|
+
this.logger.log(`Check the full report at: ${this.reportPath}`);
|
|
71
|
+
|
|
72
|
+
// Mostrar los primeros 5 problemas como ejemplo
|
|
73
|
+
this.logger.log('Sample issues:');
|
|
74
|
+
for (let i = 0; i < Math.min(5, this.issues.length); i++) {
|
|
75
|
+
const issue = this.issues[i];
|
|
76
|
+
this.logger.log(`- ${issue.type}: ${issue.message}`);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
this.logger.log('No issues found. Migration should proceed smoothly.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
success: this.issues.length === 0,
|
|
84
|
+
issueCount: this.issues.length,
|
|
85
|
+
reportPath: this.reportPath
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.logger.error(`Error during validation: ${error.message}`, error.stack);
|
|
89
|
+
this.issues.push({
|
|
90
|
+
type: 'VALIDATION_ERROR',
|
|
91
|
+
message: `Validation process failed: ${error.message}`,
|
|
92
|
+
details: error.stack
|
|
93
|
+
});
|
|
94
|
+
this.saveReport();
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
issueCount: this.issues.length,
|
|
98
|
+
reportPath: this.reportPath
|
|
99
|
+
};
|
|
100
|
+
} finally {
|
|
101
|
+
await this.cleanup();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async validateDatabaseConnections() {
|
|
106
|
+
this.logger.log('Validating database connections');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Verificar conexión a la base de datos de origen
|
|
110
|
+
await this.sourcePool.query('SELECT 1');
|
|
111
|
+
this.logger.log('Source database connection successful');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
this.logger.error(`Source database connection failed: ${error.message}`);
|
|
114
|
+
this.issues.push({
|
|
115
|
+
type: 'CONNECTION_ERROR',
|
|
116
|
+
source: 'source',
|
|
117
|
+
message: `Cannot connect to source database: ${error.message}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
// Verificar conexión a la base de datos de destino
|
|
123
|
+
await this.targetPool.query('SELECT 1');
|
|
124
|
+
this.logger.log('Target database connection successful');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.logger.error(`Target database connection failed: ${error.message}`);
|
|
127
|
+
this.issues.push({
|
|
128
|
+
type: 'CONNECTION_ERROR',
|
|
129
|
+
source: 'target',
|
|
130
|
+
message: `Cannot connect to target database: ${error.message}`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async validateApiKeys() {
|
|
136
|
+
this.logger.log('Validating API keys');
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Verificar que existan API keys en la base de datos de origen
|
|
140
|
+
const result = await this.sourcePool.query(`
|
|
141
|
+
SELECT COUNT(*) as count FROM api_keys
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
const count = parseInt(result.rows[0].count);
|
|
145
|
+
|
|
146
|
+
if (count === 0) {
|
|
147
|
+
this.logger.warn('No API keys found in source database');
|
|
148
|
+
this.issues.push({
|
|
149
|
+
type: 'DATA_ERROR',
|
|
150
|
+
message: 'No API keys found in source database. Migration will not create any tenant schemas.'
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
this.logger.log(`Found ${count} API keys in source database`);
|
|
154
|
+
}
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.logger.error(`Error validating API keys: ${error.message}`);
|
|
157
|
+
this.issues.push({
|
|
158
|
+
type: 'VALIDATION_ERROR',
|
|
159
|
+
message: `Error validating API keys: ${error.message}`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async validateDataTypeCompatibility() {
|
|
165
|
+
this.logger.log('Validating data type compatibility');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Obtener todas las tablas de la base de datos de origen
|
|
169
|
+
const tablesResult = await this.sourcePool.query(`
|
|
170
|
+
SELECT table_name
|
|
171
|
+
FROM information_schema.tables
|
|
172
|
+
WHERE table_schema = 'public'
|
|
173
|
+
AND table_type = 'BASE TABLE'
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
for (const tableRow of tablesResult.rows) {
|
|
177
|
+
const tableName = tableRow.table_name;
|
|
178
|
+
|
|
179
|
+
// Obtener columnas de la tabla en la base de datos de origen
|
|
180
|
+
const sourceColumnsResult = await this.sourcePool.query(`
|
|
181
|
+
SELECT column_name, data_type, udt_name
|
|
182
|
+
FROM information_schema.columns
|
|
183
|
+
WHERE table_schema = 'public'
|
|
184
|
+
AND table_name = $1
|
|
185
|
+
`, [tableName]);
|
|
186
|
+
|
|
187
|
+
// Verificar si la tabla existe en la base de datos de destino (schema public)
|
|
188
|
+
const targetTableExists = await this.targetPool.query(`
|
|
189
|
+
SELECT 1
|
|
190
|
+
FROM information_schema.tables
|
|
191
|
+
WHERE table_schema = 'public'
|
|
192
|
+
AND table_name = $1
|
|
193
|
+
LIMIT 1
|
|
194
|
+
`, [tableName]);
|
|
195
|
+
|
|
196
|
+
if (targetTableExists.rows.length === 0) {
|
|
197
|
+
// Si la tabla no existe en el destino, no hay problema de compatibilidad
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Obtener columnas de la tabla en la base de datos de destino
|
|
202
|
+
const targetColumnsResult = await this.targetPool.query(`
|
|
203
|
+
SELECT column_name, data_type, udt_name
|
|
204
|
+
FROM information_schema.columns
|
|
205
|
+
WHERE table_schema = 'public'
|
|
206
|
+
AND table_name = $1
|
|
207
|
+
`, [tableName]);
|
|
208
|
+
|
|
209
|
+
// Crear mapas para facilitar la comparación
|
|
210
|
+
const sourceColumns = sourceColumnsResult.rows.reduce((map, col) => {
|
|
211
|
+
map[col.column_name] = col;
|
|
212
|
+
return map;
|
|
213
|
+
}, {});
|
|
214
|
+
|
|
215
|
+
const targetColumns = targetColumnsResult.rows.reduce((map, col) => {
|
|
216
|
+
map[col.column_name] = col;
|
|
217
|
+
return map;
|
|
218
|
+
}, {});
|
|
219
|
+
|
|
220
|
+
// Comparar tipos de datos
|
|
221
|
+
for (const columnName in sourceColumns) {
|
|
222
|
+
if (targetColumns[columnName]) {
|
|
223
|
+
const sourceType = sourceColumns[columnName].data_type;
|
|
224
|
+
const targetType = targetColumns[columnName].data_type;
|
|
225
|
+
|
|
226
|
+
// Verificar si los tipos son compatibles
|
|
227
|
+
if (sourceType !== targetType) {
|
|
228
|
+
// Algunos tipos pueden ser compatibles aunque tengan nombres diferentes
|
|
229
|
+
const isCompatible = this.areTypesCompatible(sourceType, targetType);
|
|
230
|
+
|
|
231
|
+
if (!isCompatible) {
|
|
232
|
+
this.logger.warn(`Data type mismatch for ${tableName}.${columnName}: ${sourceType} (source) vs ${targetType} (target)`);
|
|
233
|
+
this.issues.push({
|
|
234
|
+
type: 'TYPE_MISMATCH',
|
|
235
|
+
table: tableName,
|
|
236
|
+
column: columnName,
|
|
237
|
+
sourceType,
|
|
238
|
+
targetType,
|
|
239
|
+
message: `Data type mismatch for ${tableName}.${columnName}: ${sourceType} (source) vs ${targetType} (target)`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.logger.error(`Error validating data type compatibility: ${error.message}`);
|
|
248
|
+
this.issues.push({
|
|
249
|
+
type: 'VALIDATION_ERROR',
|
|
250
|
+
message: `Error validating data type compatibility: ${error.message}`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private areTypesCompatible(sourceType: string, targetType: string): boolean {
|
|
256
|
+
// Definir pares de tipos compatibles
|
|
257
|
+
const compatibleTypes = [
|
|
258
|
+
['character varying', 'varchar'],
|
|
259
|
+
['character', 'char'],
|
|
260
|
+
['integer', 'int'],
|
|
261
|
+
['boolean', 'bool'],
|
|
262
|
+
['double precision', 'float8'],
|
|
263
|
+
['real', 'float4'],
|
|
264
|
+
['timestamp without time zone', 'timestamp'],
|
|
265
|
+
['timestamp with time zone', 'timestamptz']
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
// Verificar si los tipos son iguales
|
|
269
|
+
if (sourceType === targetType) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Verificar si los tipos son compatibles
|
|
274
|
+
return compatibleTypes.some(pair =>
|
|
275
|
+
(pair[0] === sourceType && pair[1] === targetType) ||
|
|
276
|
+
(pair[0] === targetType && pair[1] === sourceType)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async validateColumnSizes() {
|
|
281
|
+
this.logger.log('Validating column sizes');
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
// Obtener todas las tablas de la base de datos de origen
|
|
285
|
+
const tablesResult = await this.sourcePool.query(`
|
|
286
|
+
SELECT table_name
|
|
287
|
+
FROM information_schema.tables
|
|
288
|
+
WHERE table_schema = 'public'
|
|
289
|
+
AND table_type = 'BASE TABLE'
|
|
290
|
+
`);
|
|
291
|
+
|
|
292
|
+
for (const tableRow of tablesResult.rows) {
|
|
293
|
+
const tableName = tableRow.table_name;
|
|
294
|
+
|
|
295
|
+
// Obtener columnas de tipo character con longitud máxima
|
|
296
|
+
const sourceColumnsResult = await this.sourcePool.query(`
|
|
297
|
+
SELECT column_name, data_type, character_maximum_length
|
|
298
|
+
FROM information_schema.columns
|
|
299
|
+
WHERE table_schema = 'public'
|
|
300
|
+
AND table_name = $1
|
|
301
|
+
AND data_type IN ('character varying', 'character', 'varchar', 'char')
|
|
302
|
+
AND character_maximum_length IS NOT NULL
|
|
303
|
+
`, [tableName]);
|
|
304
|
+
|
|
305
|
+
// Verificar si la tabla existe en la base de datos de destino (schema public)
|
|
306
|
+
const targetTableExists = await this.targetPool.query(`
|
|
307
|
+
SELECT 1
|
|
308
|
+
FROM information_schema.tables
|
|
309
|
+
WHERE table_schema = 'public'
|
|
310
|
+
AND table_name = $1
|
|
311
|
+
LIMIT 1
|
|
312
|
+
`, [tableName]);
|
|
313
|
+
|
|
314
|
+
if (targetTableExists.rows.length === 0) {
|
|
315
|
+
// Si la tabla no existe en el destino, no hay problema de tamaño
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Para cada columna, verificar si el tamaño en el destino es suficiente
|
|
320
|
+
for (const sourceColumn of sourceColumnsResult.rows) {
|
|
321
|
+
const columnName = sourceColumn.column_name;
|
|
322
|
+
const sourceMaxLength = sourceColumn.character_maximum_length;
|
|
323
|
+
|
|
324
|
+
// Obtener la columna correspondiente en el destino
|
|
325
|
+
const targetColumnResult = await this.targetPool.query(`
|
|
326
|
+
SELECT character_maximum_length
|
|
327
|
+
FROM information_schema.columns
|
|
328
|
+
WHERE table_schema = 'public'
|
|
329
|
+
AND table_name = $1
|
|
330
|
+
AND column_name = $2
|
|
331
|
+
AND data_type IN ('character varying', 'character', 'varchar', 'char')
|
|
332
|
+
`, [tableName, columnName]);
|
|
333
|
+
|
|
334
|
+
if (targetColumnResult.rows.length > 0) {
|
|
335
|
+
const targetMaxLength = targetColumnResult.rows[0].character_maximum_length;
|
|
336
|
+
|
|
337
|
+
// Si el tamaño en el destino es menor, podría haber truncamiento
|
|
338
|
+
if (targetMaxLength < sourceMaxLength) {
|
|
339
|
+
this.logger.warn(`Column size mismatch for ${tableName}.${columnName}: ${sourceMaxLength} (source) vs ${targetMaxLength} (target)`);
|
|
340
|
+
this.issues.push({
|
|
341
|
+
type: 'SIZE_MISMATCH',
|
|
342
|
+
table: tableName,
|
|
343
|
+
column: columnName,
|
|
344
|
+
sourceSize: sourceMaxLength,
|
|
345
|
+
targetSize: targetMaxLength,
|
|
346
|
+
message: `Column size mismatch for ${tableName}.${columnName}: ${sourceMaxLength} (source) vs ${targetMaxLength} (target). Data may be truncated.`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.logger.error(`Error validating column sizes: ${error.message}`);
|
|
354
|
+
this.issues.push({
|
|
355
|
+
type: 'VALIDATION_ERROR',
|
|
356
|
+
message: `Error validating column sizes: ${error.message}`
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async validateEnumValues() {
|
|
362
|
+
this.logger.log('Validating enum values');
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
// Obtener todos los tipos enum en la base de datos de origen
|
|
366
|
+
const sourceEnumsResult = await this.sourcePool.query(`
|
|
367
|
+
SELECT t.typname AS enum_name
|
|
368
|
+
FROM pg_type t
|
|
369
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
370
|
+
WHERE t.typtype = 'e'
|
|
371
|
+
AND n.nspname = 'public'
|
|
372
|
+
`);
|
|
373
|
+
|
|
374
|
+
// Para cada tipo enum, obtener sus valores
|
|
375
|
+
for (const enumRow of sourceEnumsResult.rows) {
|
|
376
|
+
const enumName = enumRow.enum_name;
|
|
377
|
+
|
|
378
|
+
// Obtener valores del enum en la base de datos de origen
|
|
379
|
+
const sourceEnumValuesResult = await this.sourcePool.query(`
|
|
380
|
+
SELECT e.enumlabel
|
|
381
|
+
FROM pg_enum e
|
|
382
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
383
|
+
WHERE t.typname = $1
|
|
384
|
+
ORDER BY e.enumsortorder
|
|
385
|
+
`, [enumName]);
|
|
386
|
+
|
|
387
|
+
const sourceEnumValues = sourceEnumValuesResult.rows.map(row => row.enumlabel);
|
|
388
|
+
|
|
389
|
+
// Verificar si el enum existe en la base de datos de destino
|
|
390
|
+
const targetEnumExistsResult = await this.targetPool.query(`
|
|
391
|
+
SELECT 1
|
|
392
|
+
FROM pg_type t
|
|
393
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
394
|
+
WHERE t.typtype = 'e'
|
|
395
|
+
AND t.typname = $1
|
|
396
|
+
AND n.nspname = 'public'
|
|
397
|
+
LIMIT 1
|
|
398
|
+
`, [enumName]);
|
|
399
|
+
|
|
400
|
+
if (targetEnumExistsResult.rows.length === 0) {
|
|
401
|
+
// Si el enum no existe en el destino, no hay problema de compatibilidad
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Obtener valores del enum en la base de datos de destino
|
|
406
|
+
const targetEnumValuesResult = await this.targetPool.query(`
|
|
407
|
+
SELECT e.enumlabel
|
|
408
|
+
FROM pg_enum e
|
|
409
|
+
JOIN pg_type t ON e.enumtypid = t.oid
|
|
410
|
+
WHERE t.typname = $1
|
|
411
|
+
ORDER BY e.enumsortorder
|
|
412
|
+
`, [enumName]);
|
|
413
|
+
|
|
414
|
+
const targetEnumValues = targetEnumValuesResult.rows.map(row => row.enumlabel);
|
|
415
|
+
|
|
416
|
+
// Verificar si todos los valores del origen existen en el destino
|
|
417
|
+
for (const sourceValue of sourceEnumValues) {
|
|
418
|
+
if (!targetEnumValues.includes(sourceValue)) {
|
|
419
|
+
this.logger.warn(`Enum value '${sourceValue}' for type ${enumName} exists in source but not in target`);
|
|
420
|
+
this.issues.push({
|
|
421
|
+
type: 'ENUM_VALUE_MISSING',
|
|
422
|
+
enumType: enumName,
|
|
423
|
+
value: sourceValue,
|
|
424
|
+
message: `Enum value '${sourceValue}' for type ${enumName} exists in source but not in target. Data migration will fail for records with this value.`
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
this.logger.error(`Error validating enum values: ${error.message}`);
|
|
431
|
+
this.issues.push({
|
|
432
|
+
type: 'VALIDATION_ERROR',
|
|
433
|
+
message: `Error validating enum values: ${error.message}`
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private async validateForeignKeyConstraints() {
|
|
439
|
+
this.logger.log('Validating foreign key constraints');
|
|
440
|
+
|
|
441
|
+
// Esta validación es más compleja y depende de la estructura específica
|
|
442
|
+
// Por ahora, solo verificamos si hay tablas con claves foráneas en el origen
|
|
443
|
+
try {
|
|
444
|
+
const foreignKeysResult = await this.sourcePool.query(`
|
|
445
|
+
SELECT
|
|
446
|
+
tc.table_name,
|
|
447
|
+
kcu.column_name,
|
|
448
|
+
ccu.table_name AS foreign_table_name,
|
|
449
|
+
ccu.column_name AS foreign_column_name
|
|
450
|
+
FROM
|
|
451
|
+
information_schema.table_constraints AS tc
|
|
452
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
453
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
454
|
+
AND tc.table_schema = kcu.table_schema
|
|
455
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
456
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
457
|
+
AND ccu.table_schema = tc.table_schema
|
|
458
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
459
|
+
AND tc.table_schema = 'public'
|
|
460
|
+
`);
|
|
461
|
+
|
|
462
|
+
this.logger.log(`Found ${foreignKeysResult.rows.length} foreign key constraints in source database`);
|
|
463
|
+
|
|
464
|
+
// Aquí podríamos hacer validaciones más específicas si es necesario
|
|
465
|
+
} catch (error) {
|
|
466
|
+
this.logger.error(`Error validating foreign key constraints: ${error.message}`);
|
|
467
|
+
this.issues.push({
|
|
468
|
+
type: 'VALIDATION_ERROR',
|
|
469
|
+
message: `Error validating foreign key constraints: ${error.message}`
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private async validateUniqueConstraints() {
|
|
475
|
+
this.logger.log('Validating unique constraints...');
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
// Obtener restricciones únicas de la base de datos de origen
|
|
479
|
+
const sourceConstraintsQuery = `
|
|
480
|
+
SELECT
|
|
481
|
+
tc.table_name,
|
|
482
|
+
kcu.column_name
|
|
483
|
+
FROM
|
|
484
|
+
information_schema.table_constraints tc
|
|
485
|
+
JOIN information_schema.key_column_usage kcu
|
|
486
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
487
|
+
WHERE
|
|
488
|
+
tc.constraint_type = 'UNIQUE'
|
|
489
|
+
AND tc.table_schema = 'public'
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
const sourceConstraints = await this.sourcePool.query(sourceConstraintsQuery);
|
|
493
|
+
|
|
494
|
+
// Para cada restricción, verificar si existe en la base de datos de destino
|
|
495
|
+
for (const constraint of sourceConstraints.rows) {
|
|
496
|
+
const { table_name, column_name } = constraint;
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
// Verificar si la columna existe en la tabla de destino
|
|
500
|
+
const columnExistsQuery = `
|
|
501
|
+
SELECT column_name
|
|
502
|
+
FROM information_schema.columns
|
|
503
|
+
WHERE table_name = $1
|
|
504
|
+
AND column_name = $2
|
|
505
|
+
AND table_schema = 'public'
|
|
506
|
+
`;
|
|
507
|
+
|
|
508
|
+
const columnExists = await this.targetPool.query(columnExistsQuery, [table_name, column_name]);
|
|
509
|
+
|
|
510
|
+
if (columnExists.rows.length === 0) {
|
|
511
|
+
// La columna no existe, registrar como advertencia pero no como error crítico
|
|
512
|
+
this.issues.push({
|
|
513
|
+
type: 'COLUMN_MISMATCH',
|
|
514
|
+
message: `Column "${column_name}" in table "${table_name}" exists in source but not in target`,
|
|
515
|
+
severity: 'WARNING',
|
|
516
|
+
details: {
|
|
517
|
+
table: table_name,
|
|
518
|
+
column: column_name,
|
|
519
|
+
constraint_type: 'UNIQUE'
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
continue; // Saltar a la siguiente restricción
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Resto del código para validar restricciones únicas...
|
|
526
|
+
} catch (error) {
|
|
527
|
+
// Capturar errores específicos de columnas que no existen
|
|
528
|
+
if (error.message.includes('does not exist')) {
|
|
529
|
+
this.issues.push({
|
|
530
|
+
type: 'COLUMN_MISMATCH',
|
|
531
|
+
message: `Error checking column "${column_name}" in table "${table_name}": ${error.message}`,
|
|
532
|
+
severity: 'WARNING',
|
|
533
|
+
details: {
|
|
534
|
+
table: table_name,
|
|
535
|
+
column: column_name,
|
|
536
|
+
error: error.message
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
throw error; // Re-lanzar otros errores
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
this.logger.log('Unique constraints validation completed');
|
|
546
|
+
} catch (error) {
|
|
547
|
+
// Convertir errores críticos en advertencias para permitir que la migración continúe
|
|
548
|
+
this.issues.push({
|
|
549
|
+
type: 'VALIDATION_ERROR',
|
|
550
|
+
message: `Error validating unique constraints: ${error.message}`,
|
|
551
|
+
severity: 'WARNING',
|
|
552
|
+
details: {
|
|
553
|
+
error: error.message,
|
|
554
|
+
stack: error.stack
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
this.logger.warn(`Error during unique constraints validation: ${error.message}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private saveReport() {
|
|
563
|
+
try {
|
|
564
|
+
const report = {
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
sourceDatabase: this.sourceUrl,
|
|
567
|
+
targetDatabase: this.targetUrl,
|
|
568
|
+
issueCount: this.issues.length,
|
|
569
|
+
issues: this.issues
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
fs.writeFileSync(
|
|
573
|
+
this.reportPath,
|
|
574
|
+
JSON.stringify(report, null, 2),
|
|
575
|
+
'utf8'
|
|
576
|
+
);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this.logger.error(`Error saving validation report: ${error.message}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private async cleanup() {
|
|
583
|
+
this.logger.log('Cleaning up database connections');
|
|
584
|
+
await this.sourcePool.end();
|
|
585
|
+
await this.targetPool.end();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Script para ejecutar desde línea de comandos
|
|
590
|
+
if (require.main === module) {
|
|
591
|
+
const run = async () => {
|
|
592
|
+
try {
|
|
593
|
+
const validator = new PreMigrationValidator();
|
|
594
|
+
const result = await validator.validate();
|
|
595
|
+
|
|
596
|
+
if (result.success) {
|
|
597
|
+
console.log('Validation successful! No issues found.');
|
|
598
|
+
process.exit(0);
|
|
599
|
+
} else {
|
|
600
|
+
console.log(`Validation completed with ${result.issueCount} issues found.`);
|
|
601
|
+
console.log(`Check the full report at: ${result.reportPath}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
} catch (error) {
|
|
605
|
+
console.error('Error:', error.message);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
run();
|
|
611
611
|
}
|