@javalabs/prisma-client 1.0.18 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/.github/CODEOWNERS +1 -0
  2. package/README.md +269 -269
  3. package/dist/index.d.ts +1 -1
  4. package/dist/prisma.service.d.ts +1 -1
  5. package/dist/scripts/add-uuid-to-table.js +32 -32
  6. package/dist/scripts/data-migration/batch-migrator.js +12 -12
  7. package/dist/scripts/data-migration/data-transformer.js +14 -14
  8. package/dist/scripts/data-migration/dependency-resolver.js +23 -23
  9. package/dist/scripts/data-migration/entity-discovery.js +68 -68
  10. package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
  11. package/dist/scripts/data-migration/migration-tool.js +5 -5
  12. package/dist/scripts/data-migration/schema-utils.js +74 -74
  13. package/dist/scripts/data-migration/typecast-manager.js +4 -4
  14. package/dist/scripts/database-initializer.js +5 -5
  15. package/dist/scripts/drop-database.js +5 -5
  16. package/dist/scripts/fix-data-types.js +53 -53
  17. package/dist/scripts/fix-enum-values.js +34 -34
  18. package/dist/scripts/fix-schema-discrepancies.js +40 -40
  19. package/dist/scripts/fix-table-indexes.js +81 -81
  20. package/dist/scripts/migrate-schema-structure.js +4 -4
  21. package/dist/scripts/migrate-uuid.js +19 -19
  22. package/dist/scripts/post-migration-validator.js +49 -49
  23. package/dist/scripts/pre-migration-validator.js +107 -107
  24. package/dist/scripts/reset-database.js +21 -21
  25. package/dist/scripts/retry-failed-migrations.js +28 -28
  26. package/dist/scripts/run-migration.js +5 -5
  27. package/dist/scripts/schema-sync.js +18 -18
  28. package/dist/scripts/sequence-sync-cli.js +55 -55
  29. package/dist/scripts/sequence-synchronizer.js +20 -20
  30. package/dist/scripts/sync-enum-types.js +30 -30
  31. package/dist/scripts/sync-enum-values.js +52 -52
  32. package/dist/scripts/truncate-database.js +10 -10
  33. package/dist/scripts/verify-migration-setup.js +10 -10
  34. package/dist/tsconfig.tsbuildinfo +1 -1
  35. package/migration-config.json +63 -63
  36. package/migration-config.json.bk +95 -95
  37. package/migrations/add_reserved_amount.sql +8 -0
  38. package/package.json +44 -44
  39. package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
  40. package/prisma/schema.prisma +601 -554
  41. package/src/index.ts +23 -23
  42. package/src/prisma-factory.service.ts +40 -40
  43. package/src/prisma.module.ts +9 -9
  44. package/src/prisma.service.ts +16 -16
  45. package/src/scripts/add-uuid-to-table.ts +138 -138
  46. package/src/scripts/create-tenant-schemas.ts +145 -145
  47. package/src/scripts/data-migration/batch-migrator.ts +248 -248
  48. package/src/scripts/data-migration/data-transformer.ts +426 -426
  49. package/src/scripts/data-migration/db-connector.ts +120 -120
  50. package/src/scripts/data-migration/dependency-resolver.ts +174 -174
  51. package/src/scripts/data-migration/entity-discovery.ts +196 -196
  52. package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
  53. package/src/scripts/data-migration/migration-config.json +63 -63
  54. package/src/scripts/data-migration/migration-tool.ts +509 -509
  55. package/src/scripts/data-migration/schema-utils.ts +248 -248
  56. package/src/scripts/data-migration/tenant-migrator.ts +201 -201
  57. package/src/scripts/data-migration/typecast-manager.ts +193 -193
  58. package/src/scripts/data-migration/types.ts +113 -113
  59. package/src/scripts/database-initializer.ts +49 -49
  60. package/src/scripts/drop-database.ts +104 -104
  61. package/src/scripts/dump-source-db.sh +61 -61
  62. package/src/scripts/encrypt-user-passwords.ts +36 -36
  63. package/src/scripts/error-handler.ts +117 -117
  64. package/src/scripts/fix-data-types.ts +241 -241
  65. package/src/scripts/fix-enum-values.ts +357 -357
  66. package/src/scripts/fix-schema-discrepancies.ts +317 -317
  67. package/src/scripts/fix-table-indexes.ts +601 -601
  68. package/src/scripts/migrate-schema-structure.ts +90 -90
  69. package/src/scripts/migrate-uuid.ts +76 -76
  70. package/src/scripts/post-migration-validator.ts +526 -526
  71. package/src/scripts/pre-migration-validator.ts +610 -610
  72. package/src/scripts/reset-database.ts +263 -263
  73. package/src/scripts/retry-failed-migrations.ts +416 -416
  74. package/src/scripts/run-migration.ts +707 -707
  75. package/src/scripts/schema-sync.ts +128 -128
  76. package/src/scripts/sequence-sync-cli.ts +416 -416
  77. package/src/scripts/sequence-synchronizer.ts +127 -127
  78. package/src/scripts/sync-enum-types.ts +170 -170
  79. package/src/scripts/sync-enum-values.ts +563 -563
  80. package/src/scripts/truncate-database.ts +123 -123
  81. package/src/scripts/verify-migration-setup.ts +135 -135
  82. package/tsconfig.json +17 -17
  83. package/dist/scripts/data-migration/dependency-manager.d.ts +0 -9
  84. package/dist/scripts/data-migration/dependency-manager.js +0 -86
  85. package/dist/scripts/data-migration/dependency-manager.js.map +0 -1
  86. package/dist/scripts/data-migration/migration-config.json +0 -63
  87. package/dist/scripts/data-migration/migration-phases.d.ts +0 -5
  88. package/dist/scripts/data-migration/migration-phases.js +0 -55
  89. package/dist/scripts/data-migration/migration-phases.js.map +0 -1
  90. package/dist/scripts/data-migration/multi-source-migrator.d.ts +0 -17
  91. package/dist/scripts/data-migration/multi-source-migrator.js +0 -130
  92. package/dist/scripts/data-migration/multi-source-migrator.js.map +0 -1
  93. package/dist/scripts/data-migration/phase-generator.d.ts +0 -15
  94. package/dist/scripts/data-migration/phase-generator.js +0 -187
  95. package/dist/scripts/data-migration/phase-generator.js.map +0 -1
  96. package/dist/scripts/data-migration.d.ts +0 -22
  97. package/dist/scripts/data-migration.js +0 -593
  98. package/dist/scripts/data-migration.js.map +0 -1
  99. package/dist/scripts/multi-db-migration.d.ts +0 -1
  100. package/dist/scripts/multi-db-migration.js +0 -55
  101. package/dist/scripts/multi-db-migration.js.map +0 -1
@@ -1,602 +1,602 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as pg from 'pg';
4
- import { Logger } from '@nestjs/common';
5
- import * as dotenv from 'dotenv';
6
-
7
- dotenv.config();
8
-
9
- interface IndexInfo {
10
- schema: string;
11
- table: string;
12
- index_name: string;
13
- column_names: string[];
14
- is_unique: boolean;
15
- is_primary: boolean;
16
- definition: string;
17
- }
18
-
19
- export class TableIndexFixer {
20
- private readonly logger = new Logger('TableIndexFixer');
21
- private readonly targetPool: pg.Pool;
22
- private readonly logDir: string;
23
- private readonly logPath: string;
24
- private fixedIndexes: any[] = [];
25
-
26
- constructor(
27
- private readonly targetUrl: string = process.env.DATABASE_URL
28
- ) {
29
- if (!this.targetUrl) {
30
- throw new Error('DATABASE_URL environment variable is required');
31
- }
32
-
33
- this.targetPool = new pg.Pool({
34
- connectionString: this.targetUrl,
35
- });
36
-
37
- // Crear directorio para logs si no existe
38
- this.logDir = path.join(process.cwd(), 'migration-logs');
39
- if (!fs.existsSync(this.logDir)) {
40
- fs.mkdirSync(this.logDir, { recursive: true });
41
- }
42
-
43
- // Crear archivo de log con timestamp
44
- const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
45
- this.logPath = path.join(this.logDir, `index-fixes-${timestamp}.json`);
46
- }
47
-
48
- async fixIndexes(): Promise<void> {
49
- try {
50
- this.logger.log('Iniciando proceso de corrección de índices');
51
-
52
- // Obtener todos los esquemas excepto los del sistema
53
- const schemas = await this.getSchemas();
54
- this.logger.log(`Encontrados ${schemas.length} esquemas para procesar`);
55
-
56
- // Para cada esquema, procesar sus índices
57
- for (const schema of schemas) {
58
- await this.fixIndexesForSchema(schema);
59
- }
60
-
61
- // Guardar el log de correcciones
62
- this.saveFixLog();
63
- this.logger.log(`Proceso de corrección de índices completado. Log guardado en: ${this.logPath}`);
64
- } catch (error) {
65
- this.logger.error(`Error durante la corrección de índices: ${error.message}`, error.stack);
66
- } finally {
67
- await this.cleanup();
68
- }
69
- }
70
-
71
- private async getSchemas(): Promise<string[]> {
72
- const result = await this.targetPool.query(`
73
- SELECT schema_name
74
- FROM information_schema.schemata
75
- WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
76
- AND schema_name NOT LIKE 'pg_%'
77
- `);
78
-
79
- return result.rows.map(row => row.schema_name);
80
- }
81
-
82
- private async fixIndexesForSchema(schema: string): Promise<void> {
83
- this.logger.log(`Procesando índices para el esquema: ${schema}`);
84
-
85
- try {
86
- // Obtener todas las tablas del esquema
87
- const tables = await this.getTablesForSchema(schema);
88
- this.logger.log(`Encontradas ${tables.length} tablas en el esquema ${schema}`);
89
-
90
- // Procesar cada tabla
91
- for (const table of tables) {
92
- await this.fixIndexesForTable(schema, table);
93
- }
94
- } catch (error) {
95
- this.logger.error(`Error procesando índices para el esquema ${schema}: ${error.message}`);
96
- }
97
- }
98
-
99
- private async getTablesForSchema(schema: string): Promise<string[]> {
100
- const result = await this.targetPool.query(`
101
- SELECT table_name
102
- FROM information_schema.tables
103
- WHERE table_schema = $1
104
- AND table_type = 'BASE TABLE'
105
- `, [schema]);
106
-
107
- return result.rows.map(row => row.table_name);
108
- }
109
-
110
- private async fixIndexesForTable(schema: string, table: string): Promise<void> {
111
- this.logger.log(`Procesando índices para la tabla ${schema}.${table}`);
112
-
113
- try {
114
- // 1. Obtener información de la tabla
115
- const tableInfo = await this.getTableInfo(schema, table);
116
-
117
- // 2. Obtener índices existentes
118
- const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
119
- this.logger.log(`Encontrados ${existingIndexes.length} índices en la tabla ${schema}.${table}`);
120
-
121
- // 3. Verificar y corregir secuencias si es necesario
122
- if (tableInfo.has_identity_column) {
123
- await this.fixSequences(schema, table);
124
- }
125
-
126
- // 4. Verificar y corregir problemas comunes de índices
127
- await this.fixCommonIndexIssues(schema, table);
128
-
129
- // 5. Verificar y corregir índices faltantes para claves foráneas
130
- await this.fixForeignKeyIndexes(schema, table);
131
-
132
- // 6. Verificar y corregir índices duplicados o redundantes
133
- // Movido al final para evitar eliminar índices recién creados
134
- await this.removeRedundantIndexes(schema, table, await this.getTableIndexes(this.targetPool, schema, table));
135
-
136
- // 7. Verificación adicional para tablas críticas (usuarios)
137
- if (table === 'users' || table === 'user' || table.includes('user')) {
138
- await this.ensureCriticalUserTableIndexes(schema, table);
139
- }
140
-
141
- } catch (error) {
142
- this.logger.error(`Error procesando índices para la tabla ${schema}.${table}: ${error.message}`);
143
- }
144
- }
145
-
146
- // Nuevo método para asegurar índices críticos en tablas de usuarios
147
- private async ensureCriticalUserTableIndexes(schema: string, table: string): Promise<void> {
148
- try {
149
- const columns = await this.getTableColumns(schema, table);
150
- const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
151
- const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
152
-
153
- // Columnas críticas que DEBEN tener índices en tablas de usuarios
154
- const criticalColumns = ['email', 'username', 'phone', 'id', 'uuid'];
155
-
156
- for (const criticalCol of criticalColumns) {
157
- const column = columns.find(col => col.column_name === criticalCol);
158
-
159
- if (column && !indexedColumns.has(column.column_name)) {
160
- // Determinar si debe ser un índice único
161
- const shouldBeUnique = ['email', 'username', 'phone', 'uuid'].includes(column.column_name);
162
-
163
- const indexInfo: IndexInfo = {
164
- schema,
165
- table,
166
- index_name: `idx_${table}_${column.column_name}`,
167
- column_names: [column.column_name],
168
- is_unique: shouldBeUnique,
169
- is_primary: false,
170
- definition: ''
171
- };
172
-
173
- this.logger.log(`Creando índice crítico para ${column.column_name} en tabla de usuarios ${schema}.${table}`);
174
- await this.createIndex(schema, table, indexInfo);
175
- }
176
- }
177
-
178
- // Verificar y corregir secuencias específicamente para tablas de usuarios
179
- await this.fixUserTableSequences(schema, table);
180
-
181
- } catch (error) {
182
- this.logger.error(`Error asegurando índices críticos para tabla de usuarios ${schema}.${table}: ${error.message}`);
183
- }
184
- }
185
-
186
- // Nuevo método para corregir secuencias en tablas de usuarios
187
- private async fixUserTableSequences(schema: string, table: string): Promise<void> {
188
- try {
189
- // Obtener todas las columnas de identidad de la tabla
190
- const columnsQuery = `
191
- SELECT column_name
192
- FROM information_schema.columns
193
- WHERE table_schema = $1
194
- AND table_name = $2
195
- AND (is_identity = 'YES' OR column_default LIKE 'nextval%')
196
- `;
197
-
198
- const columnsResult = await this.targetPool.query(columnsQuery, [schema, table]);
199
- const identityColumns = columnsResult.rows.map(row => row.column_name);
200
-
201
- for (const column of identityColumns) {
202
- // Obtener el nombre de la secuencia
203
- const sequenceQuery = `
204
- SELECT pg_get_serial_sequence($1, $2) AS sequence_name
205
- `;
206
-
207
- const sequenceResult = await this.targetPool.query(sequenceQuery, [`${schema}.${table}`, column]);
208
- const sequenceName = sequenceResult.rows[0]?.sequence_name;
209
-
210
- if (sequenceName) {
211
- // Restablecer la secuencia al valor máximo actual + 1 con is_called=true para asegurar que el próximo valor sea correcto
212
- const resetQuery = `
213
- SELECT setval($1, COALESCE((SELECT MAX(${column}) FROM ${schema}.${table}), 0) + 1, true)
214
- `;
215
-
216
- await this.targetPool.query(resetQuery, [sequenceName]);
217
-
218
- this.logger.log(`Restablecida secuencia ${sequenceName} para la columna ${column} en ${schema}.${table} (tabla de usuarios)`);
219
-
220
- // Registrar la corrección
221
- this.fixedIndexes.push({
222
- schema,
223
- table,
224
- column,
225
- sequence_name: sequenceName,
226
- action: 'reset_sequence_user_table',
227
- timestamp: new Date().toISOString()
228
- });
229
- }
230
- }
231
- } catch (error) {
232
- this.logger.error(`Error restableciendo secuencias para tabla de usuarios ${schema}.${table}: ${error.message}`);
233
- }
234
- }
235
-
236
- private async removeRedundantIndexes(schema: string, table: string, indexes: IndexInfo[]): Promise<void> {
237
- // Identificar índices redundantes (un índice es redundante si existe otro que cubre las mismas columnas)
238
- const redundantIndexes: IndexInfo[] = [];
239
-
240
- // Lista de nombres de índices críticos que nunca deben eliminarse
241
- const criticalIndexPatterns = [
242
- 'email', 'username', 'phone', 'uuid', 'pkey', 'primary',
243
- 'unique', 'user_id', 'auth', 'session', 'token'
244
- ];
245
-
246
- for (let i = 0; i < indexes.length; i++) {
247
- for (let j = 0; j < indexes.length; j++) {
248
- if (i !== j && !indexes[i].is_primary && !indexes[j].is_primary) {
249
- const index1 = indexes[i];
250
- const index2 = indexes[j];
251
-
252
- // Verificar que column_names sea un array en ambos índices
253
- if (!Array.isArray(index1.column_names) || !Array.isArray(index2.column_names)) {
254
- this.logger.warn(`Índice con formato incorrecto en ${schema}.${table}: ${index1.index_name} o ${index2.index_name}`);
255
- continue;
256
- }
257
-
258
- // Verificar si el índice es crítico (nunca debe eliminarse)
259
- const isIndex1Critical = criticalIndexPatterns.some(pattern =>
260
- index1.index_name.toLowerCase().includes(pattern)
261
- );
262
-
263
- if (isIndex1Critical) {
264
- // No eliminar índices críticos
265
- continue;
266
- }
267
-
268
- // Si todas las columnas de index1 están en index2 y index2 tiene más columnas, index1 es redundante
269
- if (index1.column_names.every(col => index2.column_names.includes(col)) &&
270
- index2.column_names.length > index1.column_names.length) {
271
- redundantIndexes.push(index1);
272
- break;
273
- }
274
- }
275
- }
276
- }
277
-
278
- // Eliminar índices redundantes
279
- for (const index of redundantIndexes) {
280
- try {
281
- const query = `DROP INDEX IF EXISTS "${schema}"."${index.index_name}"`;
282
- await this.targetPool.query(query);
283
-
284
- this.logger.log(`Eliminado índice redundante ${index.index_name} en ${schema}.${table}`);
285
-
286
- // Registrar la corrección
287
- this.fixedIndexes.push({
288
- schema,
289
- table,
290
- index_name: index.index_name,
291
- action: 'removed_redundant',
292
- timestamp: new Date().toISOString()
293
- });
294
- } catch (error) {
295
- this.logger.error(`Error eliminando índice redundante ${index.index_name} en ${schema}.${table}: ${error.message}`);
296
- }
297
- }
298
- }
299
-
300
- private async fixSequences(schema: string, table: string): Promise<void> {
301
- try {
302
- // 1. Obtener todas las columnas de identidad de la tabla
303
- const columnsQuery = `
304
- SELECT column_name
305
- FROM information_schema.columns
306
- WHERE table_schema = $1
307
- AND table_name = $2
308
- AND (is_identity = 'YES' OR column_default LIKE 'nextval%')
309
- `;
310
-
311
- const columnsResult = await this.targetPool.query(columnsQuery, [schema, table]);
312
- const identityColumns = columnsResult.rows.map(row => row.column_name);
313
-
314
- // 2. Para cada columna de identidad, restablecer la secuencia
315
- for (const column of identityColumns) {
316
- // Obtener el nombre de la secuencia
317
- const sequenceQuery = `
318
- SELECT pg_get_serial_sequence($1, $2) AS sequence_name
319
- `;
320
-
321
- const sequenceResult = await this.targetPool.query(sequenceQuery, [`${schema}.${table}`, column]);
322
- const sequenceName = sequenceResult.rows[0]?.sequence_name;
323
-
324
- if (sequenceName) {
325
- // Restablecer la secuencia al valor máximo actual + 1
326
- const resetQuery = `
327
- SELECT setval($1, COALESCE((SELECT MAX(${column}) FROM ${schema}.${table}), 0) + 1, true)
328
- `;
329
-
330
- await this.targetPool.query(resetQuery, [sequenceName]);
331
-
332
- this.logger.log(`Restablecida secuencia ${sequenceName} para la columna ${column} en ${schema}.${table}`);
333
-
334
- // Registrar la corrección
335
- this.fixedIndexes.push({
336
- schema,
337
- table,
338
- column,
339
- sequence_name: sequenceName,
340
- action: 'reset_sequence',
341
- timestamp: new Date().toISOString()
342
- });
343
- }
344
- }
345
- } catch (error) {
346
- this.logger.error(`Error restableciendo secuencias para ${schema}.${table}: ${error.message}`);
347
- }
348
- }
349
-
350
- private async getTableInfo(schema: string, table: string): Promise<any> {
351
- try {
352
- // Verificar si la tabla tiene columnas de identidad (serial, bigserial, etc.)
353
- const identityQuery = `
354
- SELECT EXISTS (
355
- SELECT 1
356
- FROM information_schema.columns
357
- WHERE table_schema = $1
358
- AND table_name = $2
359
- AND is_identity = 'YES'
360
- ) AS has_identity_column;
361
- `;
362
-
363
- const result = await this.targetPool.query(identityQuery, [schema, table]);
364
- return {
365
- has_identity_column: result.rows[0]?.has_identity_column || false
366
- };
367
- } catch (error) {
368
- this.logger.error(`Error obteniendo información de la tabla ${schema}.${table}: ${error.message}`);
369
- return { has_identity_column: false };
370
- }
371
- }
372
-
373
- private async getTableIndexes(pool: pg.Pool, schema: string, table: string): Promise<IndexInfo[]> {
374
- // Esta consulta obtiene información detallada sobre los índices de una tabla
375
- const query = `
376
- SELECT
377
- i.relname AS index_name,
378
- array_agg(a.attname) AS column_names,
379
- ix.indisunique AS is_unique,
380
- ix.indisprimary AS is_primary,
381
- pg_get_indexdef(ix.indexrelid) AS definition
382
- FROM
383
- pg_index ix
384
- JOIN pg_class i ON i.oid = ix.indexrelid
385
- JOIN pg_class t ON t.oid = ix.indrelid
386
- JOIN pg_namespace n ON n.oid = t.relnamespace
387
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
388
- WHERE
389
- n.nspname = $1
390
- AND t.relname = $2
391
- GROUP BY
392
- i.relname, ix.indisunique, ix.indisprimary, ix.indexrelid
393
- ORDER BY
394
- i.relname;
395
- `;
396
-
397
- try {
398
- const result = await pool.query(query, [schema, table]);
399
- return result.rows.map(row => ({
400
- schema,
401
- table,
402
- index_name: row.index_name,
403
- column_names: row.column_names,
404
- is_unique: row.is_unique,
405
- is_primary: row.is_primary,
406
- definition: row.definition
407
- }));
408
- } catch (error) {
409
- this.logger.error(`Error obteniendo índices para ${schema}.${table}: ${error.message}`);
410
- return [];
411
- }
412
- }
413
-
414
- private async createIndex(schema: string, table: string, indexInfo: IndexInfo): Promise<void> {
415
- try {
416
- // Generar un nombre único para el índice
417
- const columnList = indexInfo.column_names.join('_');
418
- const indexName = `idx_${table}_${columnList}`.substring(0, 63); // Limitar longitud a 63 caracteres
419
-
420
- // Construir la consulta SQL para crear el índice
421
- let query = `CREATE`;
422
- if (indexInfo.is_unique) {
423
- query += ` UNIQUE`;
424
- }
425
- query += ` INDEX IF NOT EXISTS "${indexName}" ON "${schema}"."${table}" (${indexInfo.column_names.map(col => `"${col}"`).join(', ')})`;
426
-
427
- // Ejecutar la consulta
428
- await this.targetPool.query(query);
429
-
430
- this.logger.log(`Creado índice ${indexName} en ${schema}.${table}`);
431
-
432
- // Registrar la corrección
433
- this.fixedIndexes.push({
434
- schema,
435
- table,
436
- index_name: indexName,
437
- columns: indexInfo.column_names,
438
- is_unique: indexInfo.is_unique,
439
- timestamp: new Date().toISOString()
440
- });
441
- } catch (error) {
442
- this.logger.error(`Error creando índice en ${schema}.${table}: ${error.message}`);
443
- }
444
- }
445
-
446
- private async fixCommonIndexIssues(schema: string, table: string): Promise<void> {
447
- try {
448
- // 1. Verificar columnas que deberían tener índices basados en convenciones de nombres
449
- const columns = await this.getTableColumns(schema, table);
450
-
451
- // Columnas que probablemente deberían tener índices
452
- const potentialIndexColumns = columns.filter(col =>
453
- col.column_name.endsWith('_id') || // Claves foráneas
454
- col.column_name === 'id' || // Clave primaria
455
- col.column_name === 'uuid' || // UUID
456
- col.column_name === 'slug' || // Slugs
457
- col.column_name === 'email' || // Emails
458
- col.column_name === 'username' || // Nombres de usuario
459
- col.column_name.includes('code') || // Códigos
460
- col.column_name === 'user_id' || // ID de usuario (común en muchas tablas)
461
- col.column_name === 'created_at' || // Campos de fecha (útiles para ordenar)
462
- col.column_name === 'updated_at' // Campos de fecha (útiles para ordenar)
463
- );
464
-
465
- // Obtener índices existentes
466
- const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
467
- const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
468
-
469
- // Crear índices para columnas que deberían tenerlos pero no los tienen
470
- for (const column of potentialIndexColumns) {
471
- if (!indexedColumns.has(column.column_name)) {
472
- // Determinar si debería ser un índice único
473
- const shouldBeUnique = ['email', 'username', 'slug', 'uuid'].includes(column.column_name);
474
-
475
- // Crear un objeto IndexInfo para la columna
476
- const indexInfo: IndexInfo = {
477
- schema,
478
- table,
479
- index_name: `idx_${table}_${column.column_name}`,
480
- column_names: [column.column_name],
481
- is_unique: shouldBeUnique,
482
- is_primary: false,
483
- definition: ''
484
- };
485
-
486
- // Crear el índice
487
- await this.createIndex(schema, table, indexInfo);
488
- }
489
- }
490
-
491
- } catch (error) {
492
- this.logger.error(`Error corrigiendo problemas comunes de índices en ${schema}.${table}: ${error.message}`);
493
- }
494
- }
495
-
496
- private async getTableColumns(schema: string, table: string): Promise<any[]> {
497
- const query = `
498
- SELECT column_name, data_type, is_nullable
499
- FROM information_schema.columns
500
- WHERE table_schema = $1
501
- AND table_name = $2
502
- `;
503
-
504
- const result = await this.targetPool.query(query, [schema, table]);
505
- return result.rows;
506
- }
507
-
508
- private async fixForeignKeyIndexes(schema: string, table: string): Promise<void> {
509
- try {
510
- // Obtener todas las claves foráneas de la tabla
511
- const fkQuery = `
512
- SELECT
513
- kcu.column_name,
514
- ccu.table_schema AS foreign_table_schema,
515
- ccu.table_name AS foreign_table_name,
516
- ccu.column_name AS foreign_column_name
517
- FROM
518
- information_schema.table_constraints AS tc
519
- JOIN information_schema.key_column_usage AS kcu
520
- ON tc.constraint_name = kcu.constraint_name
521
- AND tc.table_schema = kcu.table_schema
522
- JOIN information_schema.constraint_column_usage AS ccu
523
- ON ccu.constraint_name = tc.constraint_name
524
- AND ccu.table_schema = tc.table_schema
525
- WHERE
526
- tc.constraint_type = 'FOREIGN KEY'
527
- AND tc.table_schema = $1
528
- AND tc.table_name = $2;
529
- `;
530
-
531
- const fkResult = await this.targetPool.query(fkQuery, [schema, table]);
532
-
533
- // Obtener índices existentes
534
- const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
535
- const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
536
-
537
- // Crear índices para columnas de clave foránea que no tienen índice
538
- for (const fk of fkResult.rows) {
539
- if (!indexedColumns.has(fk.column_name)) {
540
- // Crear un objeto IndexInfo para la columna de clave foránea
541
- const indexInfo: IndexInfo = {
542
- schema,
543
- table,
544
- index_name: `idx_${table}_${fk.column_name}`,
545
- column_names: [fk.column_name],
546
- is_unique: false,
547
- is_primary: false,
548
- definition: ''
549
- };
550
-
551
- // Crear el índice
552
- await this.createIndex(schema, table, indexInfo);
553
- }
554
- }
555
- } catch (error) {
556
- this.logger.error(`Error creando índices para claves foráneas en ${schema}.${table}: ${error.message}`);
557
- }
558
- }
559
-
560
- private saveFixLog(): void {
561
- try {
562
- const logData = {
563
- timestamp: new Date().toISOString(),
564
- targetDatabase: this.targetUrl,
565
- fixCount: this.fixedIndexes.length,
566
- fixes: this.fixedIndexes
567
- };
568
-
569
- fs.writeFileSync(
570
- this.logPath,
571
- JSON.stringify(logData, null, 2),
572
- 'utf8'
573
- );
574
- } catch (error) {
575
- this.logger.error(`Error guardando log de correcciones: ${error.message}`);
576
- }
577
- }
578
-
579
- private async cleanup(): Promise<void> {
580
- try {
581
- await this.targetPool.end();
582
- } catch (error) {
583
- this.logger.error(`Error durante la limpieza: ${error.message}`);
584
- }
585
- }
586
- }
587
-
588
- // Script para ejecutar desde línea de comandos
589
- if (require.main === module) {
590
- const run = async () => {
591
- try {
592
- const indexFixer = new TableIndexFixer();
593
- await indexFixer.fixIndexes();
594
- process.exit(0);
595
- } catch (error) {
596
- console.error("Error:", error.message);
597
- process.exit(1);
598
- }
599
- };
600
-
601
- run();
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as pg from 'pg';
4
+ import { Logger } from '@nestjs/common';
5
+ import * as dotenv from 'dotenv';
6
+
7
+ dotenv.config();
8
+
9
+ interface IndexInfo {
10
+ schema: string;
11
+ table: string;
12
+ index_name: string;
13
+ column_names: string[];
14
+ is_unique: boolean;
15
+ is_primary: boolean;
16
+ definition: string;
17
+ }
18
+
19
+ export class TableIndexFixer {
20
+ private readonly logger = new Logger('TableIndexFixer');
21
+ private readonly targetPool: pg.Pool;
22
+ private readonly logDir: string;
23
+ private readonly logPath: string;
24
+ private fixedIndexes: any[] = [];
25
+
26
+ constructor(
27
+ private readonly targetUrl: string = process.env.DATABASE_URL
28
+ ) {
29
+ if (!this.targetUrl) {
30
+ throw new Error('DATABASE_URL environment variable is required');
31
+ }
32
+
33
+ this.targetPool = new pg.Pool({
34
+ connectionString: this.targetUrl,
35
+ });
36
+
37
+ // Crear directorio para logs si no existe
38
+ this.logDir = path.join(process.cwd(), 'migration-logs');
39
+ if (!fs.existsSync(this.logDir)) {
40
+ fs.mkdirSync(this.logDir, { recursive: true });
41
+ }
42
+
43
+ // Crear archivo de log con timestamp
44
+ const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
45
+ this.logPath = path.join(this.logDir, `index-fixes-${timestamp}.json`);
46
+ }
47
+
48
+ async fixIndexes(): Promise<void> {
49
+ try {
50
+ this.logger.log('Iniciando proceso de corrección de índices');
51
+
52
+ // Obtener todos los esquemas excepto los del sistema
53
+ const schemas = await this.getSchemas();
54
+ this.logger.log(`Encontrados ${schemas.length} esquemas para procesar`);
55
+
56
+ // Para cada esquema, procesar sus índices
57
+ for (const schema of schemas) {
58
+ await this.fixIndexesForSchema(schema);
59
+ }
60
+
61
+ // Guardar el log de correcciones
62
+ this.saveFixLog();
63
+ this.logger.log(`Proceso de corrección de índices completado. Log guardado en: ${this.logPath}`);
64
+ } catch (error) {
65
+ this.logger.error(`Error durante la corrección de índices: ${error.message}`, error.stack);
66
+ } finally {
67
+ await this.cleanup();
68
+ }
69
+ }
70
+
71
+ private async getSchemas(): Promise<string[]> {
72
+ const result = await this.targetPool.query(`
73
+ SELECT schema_name
74
+ FROM information_schema.schemata
75
+ WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')
76
+ AND schema_name NOT LIKE 'pg_%'
77
+ `);
78
+
79
+ return result.rows.map(row => row.schema_name);
80
+ }
81
+
82
+ private async fixIndexesForSchema(schema: string): Promise<void> {
83
+ this.logger.log(`Procesando índices para el esquema: ${schema}`);
84
+
85
+ try {
86
+ // Obtener todas las tablas del esquema
87
+ const tables = await this.getTablesForSchema(schema);
88
+ this.logger.log(`Encontradas ${tables.length} tablas en el esquema ${schema}`);
89
+
90
+ // Procesar cada tabla
91
+ for (const table of tables) {
92
+ await this.fixIndexesForTable(schema, table);
93
+ }
94
+ } catch (error) {
95
+ this.logger.error(`Error procesando índices para el esquema ${schema}: ${error.message}`);
96
+ }
97
+ }
98
+
99
+ private async getTablesForSchema(schema: string): Promise<string[]> {
100
+ const result = await this.targetPool.query(`
101
+ SELECT table_name
102
+ FROM information_schema.tables
103
+ WHERE table_schema = $1
104
+ AND table_type = 'BASE TABLE'
105
+ `, [schema]);
106
+
107
+ return result.rows.map(row => row.table_name);
108
+ }
109
+
110
+ private async fixIndexesForTable(schema: string, table: string): Promise<void> {
111
+ this.logger.log(`Procesando índices para la tabla ${schema}.${table}`);
112
+
113
+ try {
114
+ // 1. Obtener información de la tabla
115
+ const tableInfo = await this.getTableInfo(schema, table);
116
+
117
+ // 2. Obtener índices existentes
118
+ const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
119
+ this.logger.log(`Encontrados ${existingIndexes.length} índices en la tabla ${schema}.${table}`);
120
+
121
+ // 3. Verificar y corregir secuencias si es necesario
122
+ if (tableInfo.has_identity_column) {
123
+ await this.fixSequences(schema, table);
124
+ }
125
+
126
+ // 4. Verificar y corregir problemas comunes de índices
127
+ await this.fixCommonIndexIssues(schema, table);
128
+
129
+ // 5. Verificar y corregir índices faltantes para claves foráneas
130
+ await this.fixForeignKeyIndexes(schema, table);
131
+
132
+ // 6. Verificar y corregir índices duplicados o redundantes
133
+ // Movido al final para evitar eliminar índices recién creados
134
+ await this.removeRedundantIndexes(schema, table, await this.getTableIndexes(this.targetPool, schema, table));
135
+
136
+ // 7. Verificación adicional para tablas críticas (usuarios)
137
+ if (table === 'users' || table === 'user' || table.includes('user')) {
138
+ await this.ensureCriticalUserTableIndexes(schema, table);
139
+ }
140
+
141
+ } catch (error) {
142
+ this.logger.error(`Error procesando índices para la tabla ${schema}.${table}: ${error.message}`);
143
+ }
144
+ }
145
+
146
+ // Nuevo método para asegurar índices críticos en tablas de usuarios
147
+ private async ensureCriticalUserTableIndexes(schema: string, table: string): Promise<void> {
148
+ try {
149
+ const columns = await this.getTableColumns(schema, table);
150
+ const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
151
+ const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
152
+
153
+ // Columnas críticas que DEBEN tener índices en tablas de usuarios
154
+ const criticalColumns = ['email', 'username', 'phone', 'id', 'uuid'];
155
+
156
+ for (const criticalCol of criticalColumns) {
157
+ const column = columns.find(col => col.column_name === criticalCol);
158
+
159
+ if (column && !indexedColumns.has(column.column_name)) {
160
+ // Determinar si debe ser un índice único
161
+ const shouldBeUnique = ['email', 'username', 'phone', 'uuid'].includes(column.column_name);
162
+
163
+ const indexInfo: IndexInfo = {
164
+ schema,
165
+ table,
166
+ index_name: `idx_${table}_${column.column_name}`,
167
+ column_names: [column.column_name],
168
+ is_unique: shouldBeUnique,
169
+ is_primary: false,
170
+ definition: ''
171
+ };
172
+
173
+ this.logger.log(`Creando índice crítico para ${column.column_name} en tabla de usuarios ${schema}.${table}`);
174
+ await this.createIndex(schema, table, indexInfo);
175
+ }
176
+ }
177
+
178
+ // Verificar y corregir secuencias específicamente para tablas de usuarios
179
+ await this.fixUserTableSequences(schema, table);
180
+
181
+ } catch (error) {
182
+ this.logger.error(`Error asegurando índices críticos para tabla de usuarios ${schema}.${table}: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ // Nuevo método para corregir secuencias en tablas de usuarios
187
+ private async fixUserTableSequences(schema: string, table: string): Promise<void> {
188
+ try {
189
+ // Obtener todas las columnas de identidad de la tabla
190
+ const columnsQuery = `
191
+ SELECT column_name
192
+ FROM information_schema.columns
193
+ WHERE table_schema = $1
194
+ AND table_name = $2
195
+ AND (is_identity = 'YES' OR column_default LIKE 'nextval%')
196
+ `;
197
+
198
+ const columnsResult = await this.targetPool.query(columnsQuery, [schema, table]);
199
+ const identityColumns = columnsResult.rows.map(row => row.column_name);
200
+
201
+ for (const column of identityColumns) {
202
+ // Obtener el nombre de la secuencia
203
+ const sequenceQuery = `
204
+ SELECT pg_get_serial_sequence($1, $2) AS sequence_name
205
+ `;
206
+
207
+ const sequenceResult = await this.targetPool.query(sequenceQuery, [`${schema}.${table}`, column]);
208
+ const sequenceName = sequenceResult.rows[0]?.sequence_name;
209
+
210
+ if (sequenceName) {
211
+ // Restablecer la secuencia al valor máximo actual + 1 con is_called=true para asegurar que el próximo valor sea correcto
212
+ const resetQuery = `
213
+ SELECT setval($1, COALESCE((SELECT MAX(${column}) FROM ${schema}.${table}), 0) + 1, true)
214
+ `;
215
+
216
+ await this.targetPool.query(resetQuery, [sequenceName]);
217
+
218
+ this.logger.log(`Restablecida secuencia ${sequenceName} para la columna ${column} en ${schema}.${table} (tabla de usuarios)`);
219
+
220
+ // Registrar la corrección
221
+ this.fixedIndexes.push({
222
+ schema,
223
+ table,
224
+ column,
225
+ sequence_name: sequenceName,
226
+ action: 'reset_sequence_user_table',
227
+ timestamp: new Date().toISOString()
228
+ });
229
+ }
230
+ }
231
+ } catch (error) {
232
+ this.logger.error(`Error restableciendo secuencias para tabla de usuarios ${schema}.${table}: ${error.message}`);
233
+ }
234
+ }
235
+
236
+ private async removeRedundantIndexes(schema: string, table: string, indexes: IndexInfo[]): Promise<void> {
237
+ // Identificar índices redundantes (un índice es redundante si existe otro que cubre las mismas columnas)
238
+ const redundantIndexes: IndexInfo[] = [];
239
+
240
+ // Lista de nombres de índices críticos que nunca deben eliminarse
241
+ const criticalIndexPatterns = [
242
+ 'email', 'username', 'phone', 'uuid', 'pkey', 'primary',
243
+ 'unique', 'user_id', 'auth', 'session', 'token'
244
+ ];
245
+
246
+ for (let i = 0; i < indexes.length; i++) {
247
+ for (let j = 0; j < indexes.length; j++) {
248
+ if (i !== j && !indexes[i].is_primary && !indexes[j].is_primary) {
249
+ const index1 = indexes[i];
250
+ const index2 = indexes[j];
251
+
252
+ // Verificar que column_names sea un array en ambos índices
253
+ if (!Array.isArray(index1.column_names) || !Array.isArray(index2.column_names)) {
254
+ this.logger.warn(`Índice con formato incorrecto en ${schema}.${table}: ${index1.index_name} o ${index2.index_name}`);
255
+ continue;
256
+ }
257
+
258
+ // Verificar si el índice es crítico (nunca debe eliminarse)
259
+ const isIndex1Critical = criticalIndexPatterns.some(pattern =>
260
+ index1.index_name.toLowerCase().includes(pattern)
261
+ );
262
+
263
+ if (isIndex1Critical) {
264
+ // No eliminar índices críticos
265
+ continue;
266
+ }
267
+
268
+ // Si todas las columnas de index1 están en index2 y index2 tiene más columnas, index1 es redundante
269
+ if (index1.column_names.every(col => index2.column_names.includes(col)) &&
270
+ index2.column_names.length > index1.column_names.length) {
271
+ redundantIndexes.push(index1);
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ // Eliminar índices redundantes
279
+ for (const index of redundantIndexes) {
280
+ try {
281
+ const query = `DROP INDEX IF EXISTS "${schema}"."${index.index_name}"`;
282
+ await this.targetPool.query(query);
283
+
284
+ this.logger.log(`Eliminado índice redundante ${index.index_name} en ${schema}.${table}`);
285
+
286
+ // Registrar la corrección
287
+ this.fixedIndexes.push({
288
+ schema,
289
+ table,
290
+ index_name: index.index_name,
291
+ action: 'removed_redundant',
292
+ timestamp: new Date().toISOString()
293
+ });
294
+ } catch (error) {
295
+ this.logger.error(`Error eliminando índice redundante ${index.index_name} en ${schema}.${table}: ${error.message}`);
296
+ }
297
+ }
298
+ }
299
+
300
+ private async fixSequences(schema: string, table: string): Promise<void> {
301
+ try {
302
+ // 1. Obtener todas las columnas de identidad de la tabla
303
+ const columnsQuery = `
304
+ SELECT column_name
305
+ FROM information_schema.columns
306
+ WHERE table_schema = $1
307
+ AND table_name = $2
308
+ AND (is_identity = 'YES' OR column_default LIKE 'nextval%')
309
+ `;
310
+
311
+ const columnsResult = await this.targetPool.query(columnsQuery, [schema, table]);
312
+ const identityColumns = columnsResult.rows.map(row => row.column_name);
313
+
314
+ // 2. Para cada columna de identidad, restablecer la secuencia
315
+ for (const column of identityColumns) {
316
+ // Obtener el nombre de la secuencia
317
+ const sequenceQuery = `
318
+ SELECT pg_get_serial_sequence($1, $2) AS sequence_name
319
+ `;
320
+
321
+ const sequenceResult = await this.targetPool.query(sequenceQuery, [`${schema}.${table}`, column]);
322
+ const sequenceName = sequenceResult.rows[0]?.sequence_name;
323
+
324
+ if (sequenceName) {
325
+ // Restablecer la secuencia al valor máximo actual + 1
326
+ const resetQuery = `
327
+ SELECT setval($1, COALESCE((SELECT MAX(${column}) FROM ${schema}.${table}), 0) + 1, true)
328
+ `;
329
+
330
+ await this.targetPool.query(resetQuery, [sequenceName]);
331
+
332
+ this.logger.log(`Restablecida secuencia ${sequenceName} para la columna ${column} en ${schema}.${table}`);
333
+
334
+ // Registrar la corrección
335
+ this.fixedIndexes.push({
336
+ schema,
337
+ table,
338
+ column,
339
+ sequence_name: sequenceName,
340
+ action: 'reset_sequence',
341
+ timestamp: new Date().toISOString()
342
+ });
343
+ }
344
+ }
345
+ } catch (error) {
346
+ this.logger.error(`Error restableciendo secuencias para ${schema}.${table}: ${error.message}`);
347
+ }
348
+ }
349
+
350
+ private async getTableInfo(schema: string, table: string): Promise<any> {
351
+ try {
352
+ // Verificar si la tabla tiene columnas de identidad (serial, bigserial, etc.)
353
+ const identityQuery = `
354
+ SELECT EXISTS (
355
+ SELECT 1
356
+ FROM information_schema.columns
357
+ WHERE table_schema = $1
358
+ AND table_name = $2
359
+ AND is_identity = 'YES'
360
+ ) AS has_identity_column;
361
+ `;
362
+
363
+ const result = await this.targetPool.query(identityQuery, [schema, table]);
364
+ return {
365
+ has_identity_column: result.rows[0]?.has_identity_column || false
366
+ };
367
+ } catch (error) {
368
+ this.logger.error(`Error obteniendo información de la tabla ${schema}.${table}: ${error.message}`);
369
+ return { has_identity_column: false };
370
+ }
371
+ }
372
+
373
+ private async getTableIndexes(pool: pg.Pool, schema: string, table: string): Promise<IndexInfo[]> {
374
+ // Esta consulta obtiene información detallada sobre los índices de una tabla
375
+ const query = `
376
+ SELECT
377
+ i.relname AS index_name,
378
+ array_agg(a.attname) AS column_names,
379
+ ix.indisunique AS is_unique,
380
+ ix.indisprimary AS is_primary,
381
+ pg_get_indexdef(ix.indexrelid) AS definition
382
+ FROM
383
+ pg_index ix
384
+ JOIN pg_class i ON i.oid = ix.indexrelid
385
+ JOIN pg_class t ON t.oid = ix.indrelid
386
+ JOIN pg_namespace n ON n.oid = t.relnamespace
387
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
388
+ WHERE
389
+ n.nspname = $1
390
+ AND t.relname = $2
391
+ GROUP BY
392
+ i.relname, ix.indisunique, ix.indisprimary, ix.indexrelid
393
+ ORDER BY
394
+ i.relname;
395
+ `;
396
+
397
+ try {
398
+ const result = await pool.query(query, [schema, table]);
399
+ return result.rows.map(row => ({
400
+ schema,
401
+ table,
402
+ index_name: row.index_name,
403
+ column_names: row.column_names,
404
+ is_unique: row.is_unique,
405
+ is_primary: row.is_primary,
406
+ definition: row.definition
407
+ }));
408
+ } catch (error) {
409
+ this.logger.error(`Error obteniendo índices para ${schema}.${table}: ${error.message}`);
410
+ return [];
411
+ }
412
+ }
413
+
414
+ private async createIndex(schema: string, table: string, indexInfo: IndexInfo): Promise<void> {
415
+ try {
416
+ // Generar un nombre único para el índice
417
+ const columnList = indexInfo.column_names.join('_');
418
+ const indexName = `idx_${table}_${columnList}`.substring(0, 63); // Limitar longitud a 63 caracteres
419
+
420
+ // Construir la consulta SQL para crear el índice
421
+ let query = `CREATE`;
422
+ if (indexInfo.is_unique) {
423
+ query += ` UNIQUE`;
424
+ }
425
+ query += ` INDEX IF NOT EXISTS "${indexName}" ON "${schema}"."${table}" (${indexInfo.column_names.map(col => `"${col}"`).join(', ')})`;
426
+
427
+ // Ejecutar la consulta
428
+ await this.targetPool.query(query);
429
+
430
+ this.logger.log(`Creado índice ${indexName} en ${schema}.${table}`);
431
+
432
+ // Registrar la corrección
433
+ this.fixedIndexes.push({
434
+ schema,
435
+ table,
436
+ index_name: indexName,
437
+ columns: indexInfo.column_names,
438
+ is_unique: indexInfo.is_unique,
439
+ timestamp: new Date().toISOString()
440
+ });
441
+ } catch (error) {
442
+ this.logger.error(`Error creando índice en ${schema}.${table}: ${error.message}`);
443
+ }
444
+ }
445
+
446
+ private async fixCommonIndexIssues(schema: string, table: string): Promise<void> {
447
+ try {
448
+ // 1. Verificar columnas que deberían tener índices basados en convenciones de nombres
449
+ const columns = await this.getTableColumns(schema, table);
450
+
451
+ // Columnas que probablemente deberían tener índices
452
+ const potentialIndexColumns = columns.filter(col =>
453
+ col.column_name.endsWith('_id') || // Claves foráneas
454
+ col.column_name === 'id' || // Clave primaria
455
+ col.column_name === 'uuid' || // UUID
456
+ col.column_name === 'slug' || // Slugs
457
+ col.column_name === 'email' || // Emails
458
+ col.column_name === 'username' || // Nombres de usuario
459
+ col.column_name.includes('code') || // Códigos
460
+ col.column_name === 'user_id' || // ID de usuario (común en muchas tablas)
461
+ col.column_name === 'created_at' || // Campos de fecha (útiles para ordenar)
462
+ col.column_name === 'updated_at' // Campos de fecha (útiles para ordenar)
463
+ );
464
+
465
+ // Obtener índices existentes
466
+ const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
467
+ const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
468
+
469
+ // Crear índices para columnas que deberían tenerlos pero no los tienen
470
+ for (const column of potentialIndexColumns) {
471
+ if (!indexedColumns.has(column.column_name)) {
472
+ // Determinar si debería ser un índice único
473
+ const shouldBeUnique = ['email', 'username', 'slug', 'uuid'].includes(column.column_name);
474
+
475
+ // Crear un objeto IndexInfo para la columna
476
+ const indexInfo: IndexInfo = {
477
+ schema,
478
+ table,
479
+ index_name: `idx_${table}_${column.column_name}`,
480
+ column_names: [column.column_name],
481
+ is_unique: shouldBeUnique,
482
+ is_primary: false,
483
+ definition: ''
484
+ };
485
+
486
+ // Crear el índice
487
+ await this.createIndex(schema, table, indexInfo);
488
+ }
489
+ }
490
+
491
+ } catch (error) {
492
+ this.logger.error(`Error corrigiendo problemas comunes de índices en ${schema}.${table}: ${error.message}`);
493
+ }
494
+ }
495
+
496
+ private async getTableColumns(schema: string, table: string): Promise<any[]> {
497
+ const query = `
498
+ SELECT column_name, data_type, is_nullable
499
+ FROM information_schema.columns
500
+ WHERE table_schema = $1
501
+ AND table_name = $2
502
+ `;
503
+
504
+ const result = await this.targetPool.query(query, [schema, table]);
505
+ return result.rows;
506
+ }
507
+
508
+ private async fixForeignKeyIndexes(schema: string, table: string): Promise<void> {
509
+ try {
510
+ // Obtener todas las claves foráneas de la tabla
511
+ const fkQuery = `
512
+ SELECT
513
+ kcu.column_name,
514
+ ccu.table_schema AS foreign_table_schema,
515
+ ccu.table_name AS foreign_table_name,
516
+ ccu.column_name AS foreign_column_name
517
+ FROM
518
+ information_schema.table_constraints AS tc
519
+ JOIN information_schema.key_column_usage AS kcu
520
+ ON tc.constraint_name = kcu.constraint_name
521
+ AND tc.table_schema = kcu.table_schema
522
+ JOIN information_schema.constraint_column_usage AS ccu
523
+ ON ccu.constraint_name = tc.constraint_name
524
+ AND ccu.table_schema = tc.table_schema
525
+ WHERE
526
+ tc.constraint_type = 'FOREIGN KEY'
527
+ AND tc.table_schema = $1
528
+ AND tc.table_name = $2;
529
+ `;
530
+
531
+ const fkResult = await this.targetPool.query(fkQuery, [schema, table]);
532
+
533
+ // Obtener índices existentes
534
+ const existingIndexes = await this.getTableIndexes(this.targetPool, schema, table);
535
+ const indexedColumns = new Set(existingIndexes.flatMap(idx => idx.column_names));
536
+
537
+ // Crear índices para columnas de clave foránea que no tienen índice
538
+ for (const fk of fkResult.rows) {
539
+ if (!indexedColumns.has(fk.column_name)) {
540
+ // Crear un objeto IndexInfo para la columna de clave foránea
541
+ const indexInfo: IndexInfo = {
542
+ schema,
543
+ table,
544
+ index_name: `idx_${table}_${fk.column_name}`,
545
+ column_names: [fk.column_name],
546
+ is_unique: false,
547
+ is_primary: false,
548
+ definition: ''
549
+ };
550
+
551
+ // Crear el índice
552
+ await this.createIndex(schema, table, indexInfo);
553
+ }
554
+ }
555
+ } catch (error) {
556
+ this.logger.error(`Error creando índices para claves foráneas en ${schema}.${table}: ${error.message}`);
557
+ }
558
+ }
559
+
560
+ private saveFixLog(): void {
561
+ try {
562
+ const logData = {
563
+ timestamp: new Date().toISOString(),
564
+ targetDatabase: this.targetUrl,
565
+ fixCount: this.fixedIndexes.length,
566
+ fixes: this.fixedIndexes
567
+ };
568
+
569
+ fs.writeFileSync(
570
+ this.logPath,
571
+ JSON.stringify(logData, null, 2),
572
+ 'utf8'
573
+ );
574
+ } catch (error) {
575
+ this.logger.error(`Error guardando log de correcciones: ${error.message}`);
576
+ }
577
+ }
578
+
579
+ private async cleanup(): Promise<void> {
580
+ try {
581
+ await this.targetPool.end();
582
+ } catch (error) {
583
+ this.logger.error(`Error durante la limpieza: ${error.message}`);
584
+ }
585
+ }
586
+ }
587
+
588
+ // Script para ejecutar desde línea de comandos
589
+ if (require.main === module) {
590
+ const run = async () => {
591
+ try {
592
+ const indexFixer = new TableIndexFixer();
593
+ await indexFixer.fixIndexes();
594
+ process.exit(0);
595
+ } catch (error) {
596
+ console.error("Error:", error.message);
597
+ process.exit(1);
598
+ }
599
+ };
600
+
601
+ run();
602
602
  }