mcp-database-inspector 2.0.1

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 (105) hide show
  1. package/README.md +197 -0
  2. package/dist/database/connection.d.ts +13 -0
  3. package/dist/database/connection.d.ts.map +1 -0
  4. package/dist/database/connection.js +155 -0
  5. package/dist/database/connection.js.map +1 -0
  6. package/dist/database/manager.d.ts +28 -0
  7. package/dist/database/manager.d.ts.map +1 -0
  8. package/dist/database/manager.js +621 -0
  9. package/dist/database/manager.js.map +1 -0
  10. package/dist/database/postgres-connection.d.ts +10 -0
  11. package/dist/database/postgres-connection.d.ts.map +1 -0
  12. package/dist/database/postgres-connection.js +113 -0
  13. package/dist/database/postgres-connection.js.map +1 -0
  14. package/dist/database/types.d.ts +84 -0
  15. package/dist/database/types.d.ts.map +1 -0
  16. package/dist/database/types.js +6 -0
  17. package/dist/database/types.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +14 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +186 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/test-defaults.d.ts +2 -0
  27. package/dist/test-defaults.d.ts.map +1 -0
  28. package/dist/test-defaults.js +57 -0
  29. package/dist/test-defaults.js.map +1 -0
  30. package/dist/tools/analyze-query.d.ts +27 -0
  31. package/dist/tools/analyze-query.d.ts.map +1 -0
  32. package/dist/tools/analyze-query.js +71 -0
  33. package/dist/tools/analyze-query.js.map +1 -0
  34. package/dist/tools/execute-query.d.ts +33 -0
  35. package/dist/tools/execute-query.d.ts.map +1 -0
  36. package/dist/tools/execute-query.js +57 -0
  37. package/dist/tools/execute-query.js.map +1 -0
  38. package/dist/tools/get-foreign-keys.d.ts +38 -0
  39. package/dist/tools/get-foreign-keys.d.ts.map +1 -0
  40. package/dist/tools/get-foreign-keys.js +391 -0
  41. package/dist/tools/get-foreign-keys.js.map +1 -0
  42. package/dist/tools/get-indexes.d.ts +38 -0
  43. package/dist/tools/get-indexes.d.ts.map +1 -0
  44. package/dist/tools/get-indexes.js +472 -0
  45. package/dist/tools/get-indexes.js.map +1 -0
  46. package/dist/tools/information-schema-query.d.ts +33 -0
  47. package/dist/tools/information-schema-query.d.ts.map +1 -0
  48. package/dist/tools/information-schema-query.js +76 -0
  49. package/dist/tools/information-schema-query.js.map +1 -0
  50. package/dist/tools/inspect-table.d.ts +38 -0
  51. package/dist/tools/inspect-table.d.ts.map +1 -0
  52. package/dist/tools/inspect-table.js +351 -0
  53. package/dist/tools/inspect-table.js.map +1 -0
  54. package/dist/tools/list-databases.d.ts +14 -0
  55. package/dist/tools/list-databases.d.ts.map +1 -0
  56. package/dist/tools/list-databases.js +83 -0
  57. package/dist/tools/list-databases.js.map +1 -0
  58. package/dist/tools/list-tables.d.ts +19 -0
  59. package/dist/tools/list-tables.d.ts.map +1 -0
  60. package/dist/tools/list-tables.js +130 -0
  61. package/dist/tools/list-tables.js.map +1 -0
  62. package/dist/utils/errors.d.ts +32 -0
  63. package/dist/utils/errors.d.ts.map +1 -0
  64. package/dist/utils/errors.js +98 -0
  65. package/dist/utils/errors.js.map +1 -0
  66. package/dist/utils/logger.d.ts +28 -0
  67. package/dist/utils/logger.d.ts.map +1 -0
  68. package/dist/utils/logger.js +132 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/dist/validators/input-validator.d.ts +76 -0
  71. package/dist/validators/input-validator.d.ts.map +1 -0
  72. package/dist/validators/input-validator.js +295 -0
  73. package/dist/validators/input-validator.js.map +1 -0
  74. package/dist/validators/query-validator.d.ts +19 -0
  75. package/dist/validators/query-validator.d.ts.map +1 -0
  76. package/dist/validators/query-validator.js +229 -0
  77. package/dist/validators/query-validator.js.map +1 -0
  78. package/enhanced_sql_prompt.md +324 -0
  79. package/examples/claude-config.json +23 -0
  80. package/examples/roo-config.json +16 -0
  81. package/package.json +42 -0
  82. package/src/database/connection.ts +165 -0
  83. package/src/database/manager.ts +682 -0
  84. package/src/database/postgres-connection.ts +123 -0
  85. package/src/database/types.ts +93 -0
  86. package/src/index.ts +136 -0
  87. package/src/server.ts +254 -0
  88. package/src/test-defaults.ts +63 -0
  89. package/src/tools/analyze-query.test.ts +100 -0
  90. package/src/tools/analyze-query.ts +112 -0
  91. package/src/tools/execute-query.ts +91 -0
  92. package/src/tools/get-foreign-keys.test.ts +51 -0
  93. package/src/tools/get-foreign-keys.ts +488 -0
  94. package/src/tools/get-indexes.test.ts +51 -0
  95. package/src/tools/get-indexes.ts +570 -0
  96. package/src/tools/information-schema-query.ts +125 -0
  97. package/src/tools/inspect-table.test.ts +59 -0
  98. package/src/tools/inspect-table.ts +440 -0
  99. package/src/tools/list-databases.ts +119 -0
  100. package/src/tools/list-tables.ts +181 -0
  101. package/src/utils/errors.ts +103 -0
  102. package/src/utils/logger.ts +158 -0
  103. package/src/validators/input-validator.ts +318 -0
  104. package/src/validators/query-validator.ts +267 -0
  105. package/tsconfig.json +30 -0
@@ -0,0 +1,682 @@
1
+ import mysql from 'mysql2/promise';
2
+ import pg from 'pg';
3
+ import { DatabaseConfig, DatabaseInfo, QueryResult, TableInfo, ColumnInfo, ForeignKeyInfo, IndexInfo, DatabaseType } from './types.js';
4
+ import { DatabaseConnection } from './connection.js';
5
+ import { QueryValidator } from '../validators/query-validator.js';
6
+ import { Logger } from '../utils/logger.js';
7
+ import { DatabaseError, ValidationError } from '../utils/errors.js';
8
+
9
+ export class DatabaseManager {
10
+ private databases: Map<string, DatabaseConfig> = new Map();
11
+ private readonly connectionTimeout: number = 30000;
12
+ private readonly maxRowLimit: number = 1000;
13
+
14
+ async addDatabase(url: string, name?: string): Promise<string> {
15
+ try {
16
+ const type = DatabaseConnection.detectDatabaseType(url);
17
+ const connectionOptions = DatabaseConnection.parseConnectionUrl(url);
18
+ const dbName = name || DatabaseConnection.extractDatabaseName(url);
19
+
20
+ // Test the connection first
21
+ const isConnectable = await DatabaseConnection.testConnection(connectionOptions);
22
+ if (!isConnectable) {
23
+ throw new DatabaseError(`Cannot connect to database: ${dbName}`);
24
+ }
25
+
26
+ const config: DatabaseConfig = {
27
+ name: dbName,
28
+ url,
29
+ type,
30
+ connection: null,
31
+ lastUsed: new Date(),
32
+ host: connectionOptions.host,
33
+ port: connectionOptions.port,
34
+ username: connectionOptions.user,
35
+ password: connectionOptions.password,
36
+ database: connectionOptions.database,
37
+ ssl: connectionOptions.ssl
38
+ };
39
+
40
+ this.databases.set(dbName, config);
41
+ Logger.info(`Added ${type} database configuration: ${dbName}`);
42
+ return dbName;
43
+ } catch (error) {
44
+ Logger.error(`Failed to add database: ${error instanceof Error ? error.message : 'Unknown error'}`);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ async removeDatabase(name: string): Promise<void> {
50
+ const config = this.databases.get(name);
51
+ if (!config) {
52
+ throw new DatabaseError(`Database not found: ${name}`);
53
+ }
54
+
55
+ if (config.connection) {
56
+ await DatabaseConnection.closeConnection(config.connection, config.type);
57
+ }
58
+
59
+ this.databases.delete(name);
60
+ Logger.info(`Removed database: ${name}`);
61
+ }
62
+
63
+ listDatabases(): DatabaseInfo[] {
64
+ return Array.from(this.databases.values()).map(config => ({
65
+ name: config.name,
66
+ type: config.type,
67
+ connected: config.connection !== null,
68
+ lastUsed: config.lastUsed,
69
+ host: config.host,
70
+ database: config.database
71
+ }));
72
+ }
73
+
74
+ getDatabaseType(dbName: string): DatabaseType {
75
+ const config = this.databases.get(dbName);
76
+ if (!config) {
77
+ throw new DatabaseError(`Database not found: ${dbName}`);
78
+ }
79
+ return config.type;
80
+ }
81
+
82
+ private async getConnection(dbName: string): Promise<any> {
83
+ const config = this.databases.get(dbName);
84
+ if (!config) {
85
+ throw new DatabaseError(`Database not found: ${dbName}`);
86
+ }
87
+
88
+ const connectionOptions = DatabaseConnection.parseConnectionUrl(config.url);
89
+ const connection = await DatabaseConnection.createConnection(connectionOptions);
90
+
91
+ config.lastUsed = new Date();
92
+ return connection;
93
+ }
94
+
95
+ async executeQuery(dbName: string, query: string, params?: any[]): Promise<QueryResult> {
96
+ const config = this.databases.get(dbName);
97
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
98
+
99
+ // Validate query is read-only
100
+ const validation = QueryValidator.validateQuery(query);
101
+ if (!validation.isValid) {
102
+ throw new ValidationError(`Query validation failed: ${validation.error}`);
103
+ }
104
+
105
+ let connection: any = null;
106
+ try {
107
+ connection = await this.getConnection(dbName);
108
+
109
+ // Add row limit to SELECT queries if not already present
110
+ const limitedQuery = this.addRowLimitToQuery(query, config.type);
111
+
112
+ const result = await DatabaseConnection.executeQuery(connection, limitedQuery, params, config.type);
113
+ return result;
114
+ } finally {
115
+ if (connection) {
116
+ await DatabaseConnection.closeConnection(connection, config.type);
117
+ }
118
+ }
119
+ }
120
+
121
+ private addRowLimitToQuery(query: string, type: DatabaseType): string {
122
+ const trimmedQuery = query.trim().toUpperCase();
123
+
124
+ if (trimmedQuery.startsWith('SELECT') && !trimmedQuery.includes('LIMIT')) {
125
+ if (type === DatabaseType.PostgreSQL) {
126
+ return `${query.trim()} LIMIT ${this.maxRowLimit}`;
127
+ }
128
+ return `${query.trim()} LIMIT ${this.maxRowLimit}`;
129
+ }
130
+
131
+ return query;
132
+ }
133
+
134
+ async getTables(dbName: string): Promise<TableInfo[]> {
135
+ const config = this.databases.get(dbName);
136
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
137
+
138
+ if (config.type === DatabaseType.PostgreSQL) {
139
+ return this.getTablesPostgres(dbName);
140
+ }
141
+
142
+ const query = `
143
+ SELECT
144
+ TABLE_NAME as tableName,
145
+ TABLE_TYPE as tableType,
146
+ ENGINE as engine,
147
+ TABLE_ROWS as tableRows,
148
+ TABLE_COMMENT as tableComment
149
+ FROM INFORMATION_SCHEMA.TABLES
150
+ WHERE TABLE_SCHEMA = ?
151
+ ORDER BY TABLE_NAME
152
+ `;
153
+
154
+ let connection: any = null;
155
+ try {
156
+ connection = await this.getConnection(dbName);
157
+ const result = await DatabaseConnection.executeQuery(connection, query, [config.database], config.type);
158
+
159
+ return result.rows.map(row => ({
160
+ tableName: row.tableName,
161
+ tableType: row.tableType,
162
+ engine: row.engine,
163
+ tableRows: row.tableRows ? parseInt(row.tableRows) : undefined,
164
+ tableComment: row.tableComment || undefined
165
+ }));
166
+ } finally {
167
+ if (connection) {
168
+ await DatabaseConnection.closeConnection(connection, config.type);
169
+ }
170
+ }
171
+ }
172
+
173
+ private async getTablesPostgres(dbName: string): Promise<TableInfo[]> {
174
+ const config = this.databases.get(dbName)!;
175
+ const query = `
176
+ SELECT
177
+ table_name as "tableName",
178
+ table_type as "tableType",
179
+ NULL as engine,
180
+ NULL as "tableRows",
181
+ NULL as "tableComment"
182
+ FROM information_schema.tables
183
+ WHERE table_schema = 'public'
184
+ OR table_schema = $1
185
+ ORDER BY table_name
186
+ `;
187
+
188
+ let connection: any = null;
189
+ try {
190
+ connection = await this.getConnection(dbName);
191
+ const result = await DatabaseConnection.executeQuery(connection, query, [config.database], config.type);
192
+
193
+ return result.rows.map(row => ({
194
+ tableName: row.tableName,
195
+ tableType: row.tableType,
196
+ engine: undefined,
197
+ tableRows: undefined,
198
+ tableComment: undefined
199
+ }));
200
+ } finally {
201
+ if (connection) {
202
+ await DatabaseConnection.closeConnection(connection, config.type);
203
+ }
204
+ }
205
+ }
206
+
207
+ async getTableSchema(dbName: string, tableName: string): Promise<ColumnInfo[]> {
208
+ const config = this.databases.get(dbName);
209
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
210
+
211
+ if (config.type === DatabaseType.PostgreSQL) {
212
+ return this.getTableSchemaPostgres(dbName, tableName);
213
+ }
214
+
215
+ const query = `
216
+ SELECT
217
+ c.COLUMN_NAME as columnName,
218
+ c.DATA_TYPE as dataType,
219
+ c.IS_NULLABLE as isNullable,
220
+ c.COLUMN_DEFAULT as columnDefault,
221
+ c.EXTRA as extra,
222
+ c.COLUMN_COMMENT as columnComment,
223
+ c.CHARACTER_MAXIMUM_LENGTH as characterMaximumLength,
224
+ c.NUMERIC_PRECISION as numericPrecision,
225
+ c.NUMERIC_SCALE as numericScale,
226
+ CASE WHEN k.COLUMN_NAME IS NOT NULL THEN true ELSE false END as isPrimaryKey
227
+ FROM INFORMATION_SCHEMA.COLUMNS c
228
+ LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
229
+ ON c.TABLE_SCHEMA = k.TABLE_SCHEMA
230
+ AND c.TABLE_NAME = k.TABLE_NAME
231
+ AND c.COLUMN_NAME = k.COLUMN_NAME
232
+ AND k.CONSTRAINT_NAME = 'PRIMARY'
233
+ WHERE c.TABLE_SCHEMA = ? AND c.TABLE_NAME = ?
234
+ ORDER BY c.ORDINAL_POSITION
235
+ `;
236
+
237
+ let connection: any = null;
238
+ try {
239
+ connection = await this.getConnection(dbName);
240
+ const result = await DatabaseConnection.executeQuery(connection, query, [config.database, tableName], config.type);
241
+
242
+ return result.rows.map(row => ({
243
+ columnName: row.columnName,
244
+ dataType: row.dataType,
245
+ isNullable: row.isNullable,
246
+ columnDefault: row.columnDefault,
247
+ isPrimaryKey: Boolean(row.isPrimaryKey),
248
+ isAutoIncrement: row.extra && row.extra.toLowerCase().includes('auto_increment'),
249
+ columnComment: row.columnComment || undefined,
250
+ characterMaximumLength: row.characterMaximumLength,
251
+ numericPrecision: row.numericPrecision,
252
+ numericScale: row.numericScale
253
+ }));
254
+ } finally {
255
+ if (connection) {
256
+ await DatabaseConnection.closeConnection(connection, config.type);
257
+ }
258
+ }
259
+ }
260
+
261
+ private async getTableSchemaPostgres(dbName: string, tableName: string): Promise<ColumnInfo[]> {
262
+ const config = this.databases.get(dbName)!;
263
+ const query = `
264
+ SELECT
265
+ c.column_name as "columnName",
266
+ c.data_type as "dataType",
267
+ c.is_nullable as "isNullable",
268
+ c.column_default as "columnDefault",
269
+ NULL as extra,
270
+ NULL as "columnComment",
271
+ c.character_maximum_length as "characterMaximumLength",
272
+ c.numeric_precision as "numericPrecision",
273
+ c.numeric_scale as "numericScale",
274
+ EXISTS (
275
+ SELECT 1 FROM information_schema.table_constraints tc
276
+ JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
277
+ WHERE tc.constraint_type = 'PRIMARY KEY'
278
+ AND tc.table_name = c.table_name
279
+ AND kcu.column_name = c.column_name
280
+ ) as "isPrimaryKey"
281
+ FROM information_schema.columns c
282
+ WHERE c.table_name = $2 AND (c.table_schema = 'public' OR c.table_schema = $1)
283
+ ORDER BY c.ordinal_position
284
+ `;
285
+
286
+ let connection: any = null;
287
+ try {
288
+ connection = await this.getConnection(dbName);
289
+ const result = await DatabaseConnection.executeQuery(connection, query, [config.database, tableName], config.type);
290
+
291
+ return result.rows.map(row => ({
292
+ columnName: row.columnName,
293
+ dataType: row.dataType,
294
+ isNullable: row.isNullable,
295
+ columnDefault: row.columnDefault,
296
+ isPrimaryKey: Boolean(row.isPrimaryKey),
297
+ isAutoIncrement: row.columnDefault && row.columnDefault.includes('nextval'),
298
+ columnComment: undefined,
299
+ characterMaximumLength: row.characterMaximumLength,
300
+ numericPrecision: row.numericPrecision,
301
+ numericScale: row.numericScale
302
+ }));
303
+ } finally {
304
+ if (connection) {
305
+ await DatabaseConnection.closeConnection(connection, config.type);
306
+ }
307
+ }
308
+ }
309
+
310
+ async getForeignKeys(dbName: string, tableName?: string): Promise<ForeignKeyInfo[]> {
311
+ const config = this.databases.get(dbName);
312
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
313
+
314
+ if (config.type === DatabaseType.PostgreSQL) {
315
+ return this.getForeignKeysPostgres(dbName, tableName);
316
+ }
317
+
318
+ let query = `
319
+ SELECT
320
+ rc.CONSTRAINT_NAME as constraintName,
321
+ kcu.TABLE_NAME as tableName,
322
+ kcu.COLUMN_NAME as columnName,
323
+ kcu.REFERENCED_TABLE_NAME as referencedTableName,
324
+ kcu.REFERENCED_COLUMN_NAME as referencedColumnName,
325
+ rc.UPDATE_RULE as updateRule,
326
+ rc.DELETE_RULE as deleteRule
327
+ FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
328
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
329
+ ON rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
330
+ AND rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
331
+ WHERE rc.CONSTRAINT_SCHEMA = ?
332
+ `;
333
+
334
+ const params: string[] = [];
335
+ params.push(config.database);
336
+
337
+ if (tableName) {
338
+ query += ' AND kcu.TABLE_NAME = ?';
339
+ params.push(tableName);
340
+ }
341
+
342
+ query += ' ORDER BY kcu.TABLE_NAME, rc.CONSTRAINT_NAME';
343
+
344
+ let connection: any = null;
345
+ try {
346
+ connection = await this.getConnection(dbName);
347
+ const result = await DatabaseConnection.executeQuery(connection, query, params, config.type);
348
+
349
+ return result.rows.map(row => ({
350
+ constraintName: row.constraintName,
351
+ tableName: row.tableName,
352
+ columnName: row.columnName,
353
+ referencedTableName: row.referencedTableName,
354
+ referencedColumnName: row.referencedColumnName,
355
+ updateRule: row.updateRule,
356
+ deleteRule: row.deleteRule
357
+ }));
358
+ } finally {
359
+ if (connection) {
360
+ await DatabaseConnection.closeConnection(connection, config.type);
361
+ }
362
+ }
363
+ }
364
+
365
+ private async getForeignKeysPostgres(dbName: string, tableName?: string): Promise<ForeignKeyInfo[]> {
366
+ const config = this.databases.get(dbName)!;
367
+ let query = `
368
+ SELECT
369
+ tc.constraint_name as "constraintName",
370
+ tc.table_name as "tableName",
371
+ kcu.column_name as "columnName",
372
+ ccu.table_name AS "referencedTableName",
373
+ ccu.column_name AS "referencedColumnName"
374
+ FROM information_schema.table_constraints AS tc
375
+ JOIN information_schema.key_column_usage AS kcu
376
+ ON tc.constraint_name = kcu.constraint_name
377
+ AND tc.table_schema = kcu.table_schema
378
+ JOIN information_schema.constraint_column_usage AS ccu
379
+ ON ccu.constraint_name = tc.constraint_name
380
+ AND ccu.table_schema = tc.table_schema
381
+ WHERE tc.constraint_type = 'FOREIGN KEY'
382
+ AND (tc.table_schema = 'public' OR tc.table_schema = $1)
383
+ `;
384
+
385
+ const params: string[] = [config.database];
386
+ if (tableName) {
387
+ query += ' AND tc.table_name = $2';
388
+ params.push(tableName);
389
+ }
390
+
391
+ let connection: any = null;
392
+ try {
393
+ connection = await this.getConnection(dbName);
394
+ const result = await DatabaseConnection.executeQuery(connection, query, params, config.type);
395
+
396
+ return result.rows.map(row => ({
397
+ constraintName: row.constraintName,
398
+ tableName: row.tableName,
399
+ columnName: row.columnName,
400
+ referencedTableName: row.referencedTableName,
401
+ referencedColumnName: row.referencedColumnName,
402
+ updateRule: undefined,
403
+ deleteRule: undefined
404
+ }));
405
+ } finally {
406
+ if (connection) {
407
+ await DatabaseConnection.closeConnection(connection, config.type);
408
+ }
409
+ }
410
+ }
411
+
412
+ async getIndexes(dbName: string, tableName: string): Promise<IndexInfo[]> {
413
+ const config = this.databases.get(dbName);
414
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
415
+
416
+ if (config.type === DatabaseType.PostgreSQL) {
417
+ return this.getIndexesPostgres(dbName, tableName);
418
+ }
419
+
420
+ const query = `
421
+ SELECT
422
+ TABLE_NAME as tableName,
423
+ INDEX_NAME as indexName,
424
+ COLUMN_NAME as columnName,
425
+ NON_UNIQUE as nonUnique,
426
+ INDEX_TYPE as indexType,
427
+ CARDINALITY as cardinality,
428
+ SUB_PART as subPart,
429
+ NULLABLE as nullable
430
+ FROM INFORMATION_SCHEMA.STATISTICS
431
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
432
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
433
+ `;
434
+
435
+ let connection: any = null;
436
+ try {
437
+ connection = await this.getConnection(dbName);
438
+ const result = await DatabaseConnection.executeQuery(connection, query, [config.database, tableName], config.type);
439
+
440
+ return result.rows.map(row => ({
441
+ tableName: row.tableName,
442
+ indexName: row.indexName,
443
+ columnName: row.columnName,
444
+ nonUnique: Boolean(row.nonUnique),
445
+ indexType: row.indexType,
446
+ cardinality: row.cardinality,
447
+ subPart: row.subPart,
448
+ nullable: row.nullable === 'YES',
449
+ isPrimary: row.indexName === 'PRIMARY'
450
+ }));
451
+ } finally {
452
+ if (connection) {
453
+ await DatabaseConnection.closeConnection(connection, config.type);
454
+ }
455
+ }
456
+ }
457
+
458
+ private async getIndexesPostgres(dbName: string, tableName: string): Promise<IndexInfo[]> {
459
+ const config = this.databases.get(dbName)!;
460
+ const query = `
461
+ SELECT
462
+ t.relname as "tableName",
463
+ i.relname as "indexName",
464
+ a.attname as "columnName",
465
+ NOT ix.indisunique as "nonUnique",
466
+ ix.indisprimary as "isPrimary",
467
+ am.amname as "indexType"
468
+ FROM pg_class t
469
+ JOIN pg_index ix ON t.oid = ix.indrelid
470
+ JOIN pg_class i ON i.oid = ix.indexrelid
471
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
472
+ JOIN pg_am am ON i.relam = am.oid
473
+ WHERE t.relname = $1
474
+ ORDER BY i.relname
475
+ `;
476
+
477
+ let connection: any = null;
478
+ try {
479
+ connection = await this.getConnection(dbName);
480
+ const result = await DatabaseConnection.executeQuery(connection, query, [tableName], config.type);
481
+
482
+ return result.rows.map(row => ({
483
+ tableName: row.tableName,
484
+ indexName: row.indexName,
485
+ columnName: row.columnName,
486
+ nonUnique: Boolean(row.nonUnique),
487
+ indexType: row.indexType,
488
+ cardinality: undefined,
489
+ subPart: undefined,
490
+ nullable: true, // pg_attribute doesn't directly tell us nullable here, but irrelevant for simple index list
491
+ isPrimary: Boolean(row.isPrimary)
492
+ }));
493
+ } finally {
494
+ if (connection) {
495
+ await DatabaseConnection.closeConnection(connection, config.type);
496
+ }
497
+ }
498
+ }
499
+
500
+ async cleanup(): Promise<void> {
501
+ Logger.info('Cleaning up database connections...');
502
+
503
+ for (const [name, config] of this.databases) {
504
+ if (config.connection) {
505
+ try {
506
+ await DatabaseConnection.closeConnection(config.connection, config.type);
507
+ } catch (error) {
508
+ Logger.warn(`Error closing connection for ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
509
+ }
510
+ }
511
+ }
512
+
513
+ this.databases.clear();
514
+ Logger.info('Database cleanup completed');
515
+ }
516
+
517
+ async queryInformationSchema(
518
+ dbName: string,
519
+ table: 'COLUMNS' | 'TABLES' | 'ROUTINES',
520
+ filters?: Record<string, string>,
521
+ limit: number = 100
522
+ ): Promise<QueryResult> {
523
+ const config = this.databases.get(dbName);
524
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
525
+
526
+ const allowedTables = ['COLUMNS', 'TABLES', 'ROUTINES'];
527
+ if (!allowedTables.includes(table)) {
528
+ throw new ValidationError(`Table '${table}' is not allowed for INFORMATION_SCHEMA queries.`);
529
+ }
530
+
531
+ let whereClauses: string[] = [];
532
+ let params: any[] = [];
533
+
534
+ if (filters) {
535
+ for (const [key, value] of Object.entries(filters)) {
536
+ if (!/^[A-Z_]+$/.test(key)) {
537
+ throw new ValidationError(`Invalid filter key: ${key}. Only uppercase letters and underscores allowed.`);
538
+ }
539
+ if (config.type === DatabaseType.PostgreSQL) {
540
+ whereClauses.push(`${key.toLowerCase()} = $${params.length + 1}`);
541
+ } else {
542
+ whereClauses.push(`${key} = ?`);
543
+ }
544
+ params.push(value);
545
+ }
546
+ }
547
+
548
+ if (config.type === DatabaseType.PostgreSQL) {
549
+ whereClauses.unshift(`table_schema = $${params.length + 1}`);
550
+ params.push('public'); // Default for PG
551
+ } else {
552
+ whereClauses.unshift('TABLE_SCHEMA = ?');
553
+ params.unshift(config.database);
554
+ }
555
+
556
+ let sql = `SELECT * FROM INFORMATION_SCHEMA.${table}`;
557
+ if (whereClauses.length > 0) {
558
+ sql += ' WHERE ' + whereClauses.join(' AND ');
559
+ }
560
+ sql += ` LIMIT ${Math.min(Math.max(limit, 1), 1000)}`;
561
+
562
+ let connection: any = null;
563
+ try {
564
+ connection = await this.getConnection(dbName);
565
+ const result = await DatabaseConnection.executeQuery(connection, sql, params, config.type);
566
+ return result;
567
+ } finally {
568
+ if (connection) {
569
+ await DatabaseConnection.closeConnection(connection, config.type);
570
+ }
571
+ }
572
+ }
573
+
574
+ async analyzeQuery(dbName: string, query: string): Promise<any> {
575
+ const config = this.databases.get(dbName);
576
+ if (!config) throw new DatabaseError(`Database not found: ${dbName}`);
577
+
578
+ // Validate query is read-only
579
+ const validation = QueryValidator.validateQuery(query);
580
+ if (!validation.isValid) {
581
+ throw new ValidationError(`Query validation failed: ${validation.error}`);
582
+ }
583
+
584
+ let explainQuery: string;
585
+ if (config.type === DatabaseType.PostgreSQL) {
586
+ explainQuery = `EXPLAIN (FORMAT JSON, VERBOSE, ANALYZE FALSE) ${query}`;
587
+ } else {
588
+ explainQuery = `EXPLAIN FORMAT=JSON ${query}`;
589
+ }
590
+
591
+ let connection: any = null;
592
+ try {
593
+ connection = await this.getConnection(dbName);
594
+ const result = await DatabaseConnection.executeQuery(connection, explainQuery, [], config.type);
595
+
596
+ let rawPlan: any;
597
+ if (config.type === DatabaseType.PostgreSQL) {
598
+ // PG returns [{ "QUERY PLAN": [...] }] or similar
599
+ const firstRow = result.rows[0];
600
+ const firstKey = Object.keys(firstRow)[0];
601
+ const val = firstRow[firstKey];
602
+ rawPlan = Array.isArray(val) ? val[0] : val;
603
+ } else {
604
+ // MySQL returns [{ EXPLAIN: "json_string" }] or similar
605
+ const firstRow = result.rows[0];
606
+ const firstKey = Object.keys(firstRow)[0];
607
+ try {
608
+ rawPlan = typeof firstRow[firstKey] === 'string' ? JSON.parse(firstRow[firstKey]) : firstRow[firstKey];
609
+ } catch (e) {
610
+ rawPlan = firstRow[firstKey];
611
+ }
612
+ }
613
+
614
+ return {
615
+ database: dbName,
616
+ type: config.type,
617
+ query,
618
+ plan: rawPlan,
619
+ summary: this.summarizePlan(rawPlan, config.type)
620
+ };
621
+ } finally {
622
+ if (connection) {
623
+ await DatabaseConnection.closeConnection(connection, config.type);
624
+ }
625
+ }
626
+ }
627
+
628
+ private summarizePlan(plan: any, type: DatabaseType): any {
629
+ const summary: any = {
630
+ cost: 0,
631
+ potentialIssues: [] as string[],
632
+ operations: [] as string[]
633
+ };
634
+
635
+ if (type === DatabaseType.PostgreSQL) {
636
+ const rootPlan = plan?.Plan || plan?.[0]?.Plan;
637
+ if (rootPlan) {
638
+ summary.cost = rootPlan['Total Cost'];
639
+ this.traversePostgresPlan(rootPlan, summary);
640
+ }
641
+ } else {
642
+ // MySQL JSON format is deeply nested under query_block
643
+ const queryBlock = plan?.query_block;
644
+ if (queryBlock) {
645
+ summary.cost = queryBlock.cost_info?.query_cost;
646
+ this.traverseMySQLPlan(queryBlock, summary);
647
+ }
648
+ }
649
+
650
+ return summary;
651
+ }
652
+
653
+ private traversePostgresPlan(plan: any, summary: any) {
654
+ const nodeType = plan['Node Type'];
655
+ summary.operations.push(nodeType);
656
+
657
+ if (nodeType === 'Seq Scan') {
658
+ summary.potentialIssues.push(`Full table scan on ${plan['Relation Name']}`);
659
+ }
660
+
661
+ if (plan.Plans) {
662
+ for (const subPlan of plan.Plans) {
663
+ this.traversePostgresPlan(subPlan, summary);
664
+ }
665
+ }
666
+ }
667
+
668
+ private traverseMySQLPlan(node: any, summary: any) {
669
+ if (node.table) {
670
+ summary.operations.push(node.table.access_type);
671
+ if (node.table.access_type === 'ALL') {
672
+ summary.potentialIssues.push(`Full table scan on ${node.table.table_name}`);
673
+ }
674
+ }
675
+
676
+ for (const key in node) {
677
+ if (typeof node[key] === 'object' && node[key] !== null) {
678
+ this.traverseMySQLPlan(node[key], summary);
679
+ }
680
+ }
681
+ }
682
+ }