@javalabs/prisma-client 1.0.16 → 1.0.19
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/.dockerignore +14 -0
- package/Dockerfile +23 -0
- package/README.md +269 -269
- package/dist/index.d.ts +1 -1
- package/dist/prisma.service.d.ts +1 -1
- package/dist/scripts/add-uuid-to-table.js +32 -32
- package/dist/scripts/data-migration/batch-migrator.js +12 -12
- package/dist/scripts/data-migration/data-transformer.js +14 -14
- package/dist/scripts/data-migration/dependency-resolver.js +23 -23
- package/dist/scripts/data-migration/entity-discovery.js +68 -68
- package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
- package/dist/scripts/data-migration/migration-tool.js +5 -5
- package/dist/scripts/data-migration/schema-utils.js +74 -74
- package/dist/scripts/data-migration/typecast-manager.js +4 -4
- package/dist/scripts/database-initializer.js +5 -5
- package/dist/scripts/drop-database.js +5 -5
- package/dist/scripts/fix-data-types.js +53 -53
- package/dist/scripts/fix-enum-values.js +34 -34
- package/dist/scripts/fix-schema-discrepancies.js +40 -40
- package/dist/scripts/fix-table-indexes.js +81 -81
- package/dist/scripts/migrate-schema-structure.js +4 -4
- package/dist/scripts/migrate-uuid.js +19 -19
- package/dist/scripts/post-migration-validator.js +49 -49
- package/dist/scripts/pre-migration-validator.js +107 -107
- package/dist/scripts/reset-database.js +21 -21
- package/dist/scripts/retry-failed-migrations.js +28 -28
- package/dist/scripts/run-migration.js +5 -5
- package/dist/scripts/schema-sync.js +18 -18
- package/dist/scripts/sequence-sync-cli.js +55 -55
- package/dist/scripts/sequence-synchronizer.js +20 -20
- package/dist/scripts/sync-enum-types.js +30 -30
- package/dist/scripts/sync-enum-values.js +52 -52
- package/dist/scripts/truncate-database.js +10 -10
- package/dist/scripts/verify-migration-setup.js +10 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migration-config.json +63 -63
- package/migration-config.json.bk +95 -95
- package/package.json +44 -44
- package/prisma/migrations/add_accepts_partial_payments_to_users.sql +19 -0
- package/prisma/migrations/add_amount_received_to_manual_payments.sql +19 -0
- package/prisma/migrations/add_commission_fields.sql +33 -0
- package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
- package/prisma/migrations/complete_partial_payments_migration.sql +53 -0
- package/prisma/migrations/create_settlements_table.sql +60 -0
- package/prisma/schema.prisma +56 -4
- 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
- package/dist/scripts/data-migration/dependency-manager.d.ts +0 -9
- package/dist/scripts/data-migration/dependency-manager.js +0 -86
- package/dist/scripts/data-migration/dependency-manager.js.map +0 -1
- package/dist/scripts/data-migration/migration-config.json +0 -63
- package/dist/scripts/data-migration/migration-phases.d.ts +0 -5
- package/dist/scripts/data-migration/migration-phases.js +0 -55
- package/dist/scripts/data-migration/migration-phases.js.map +0 -1
- package/dist/scripts/data-migration/multi-source-migrator.d.ts +0 -17
- package/dist/scripts/data-migration/multi-source-migrator.js +0 -130
- package/dist/scripts/data-migration/multi-source-migrator.js.map +0 -1
- package/dist/scripts/data-migration/phase-generator.d.ts +0 -15
- package/dist/scripts/data-migration/phase-generator.js +0 -187
- package/dist/scripts/data-migration/phase-generator.js.map +0 -1
- package/dist/scripts/data-migration.d.ts +0 -22
- package/dist/scripts/data-migration.js +0 -593
- package/dist/scripts/data-migration.js.map +0 -1
- package/dist/scripts/multi-db-migration.d.ts +0 -1
- package/dist/scripts/multi-db-migration.js +0 -55
- 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
|
}
|