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,488 @@
1
+ import { z } from 'zod';
2
+ import { DatabaseManager } from '../database/manager.js';
3
+ import { InputValidator } from '../validators/input-validator.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { ToolError } from '../utils/errors.js';
6
+
7
+ // Tool schema
8
+ const GetForeignKeysArgsSchema = z.object({
9
+ database: z.string().min(1, 'Database name is required'),
10
+ table: z.string().optional(),
11
+ tables: z.array(z.string().min(1)).optional()
12
+ }).superRefine((data, ctx) => {
13
+ const hasTable = typeof data.table === 'string' && data.table.length > 0;
14
+ const hasTables = Array.isArray(data.tables) && data.tables.length > 0;
15
+ if (hasTable && hasTables) {
16
+ ctx.addIssue({
17
+ code: z.ZodIssueCode.custom,
18
+ message: "Provide either 'table' or 'tables', not both"
19
+ });
20
+ } else if (!hasTable && !hasTables) {
21
+ ctx.addIssue({
22
+ code: z.ZodIssueCode.custom,
23
+ message: "Either 'table' or non-empty 'tables' must be provided"
24
+ });
25
+ }
26
+ });
27
+
28
+ /**
29
+ * Tool: get_foreign_keys
30
+ * Get foreign key relationships for one or more tables, or the entire database.
31
+ *
32
+ * Supports both single-table and multi-table inspection:
33
+ * - Provide either `table` (string) for a single table, or `tables` (string[]) for multiple tables.
34
+ * - If `tables` is provided, returns a mapping of table names to their foreign key analysis.
35
+ * - Do not provide both `table` and `tables` at the same time.
36
+ */
37
+ export interface GetForeignKeysTool {
38
+ name: 'get_foreign_keys';
39
+ description: 'Get foreign key relationships for one or more tables, or the entire database. Supports multi-table inspection via the tables: string[] parameter.';
40
+ inputSchema: {
41
+ type: 'object';
42
+ properties: {
43
+ database: {
44
+ type: 'string';
45
+ description: 'Name of the database to analyze';
46
+ };
47
+ table: {
48
+ type: 'string';
49
+ description: 'Specific table name to get foreign keys for (single-table mode)';
50
+ };
51
+ tables: {
52
+ type: 'array';
53
+ items: { type: 'string' };
54
+ description: 'Array of table names to get foreign keys for (multi-table mode)';
55
+ };
56
+ };
57
+ required: ['database'];
58
+ };
59
+ }
60
+
61
+ export const getForeignKeysToolDefinition: GetForeignKeysTool = {
62
+ name: 'get_foreign_keys',
63
+ description: 'Get foreign key relationships for one or more tables, or the entire database. Supports multi-table inspection via the tables: string[] parameter.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ database: {
68
+ type: 'string',
69
+ description: 'Name of the database to analyze'
70
+ },
71
+ table: {
72
+ type: 'string',
73
+ description: 'Specific table name to get foreign keys for (single-table mode)'
74
+ },
75
+ tables: {
76
+ type: 'array',
77
+ items: { type: 'string' },
78
+ description: 'Array of table names to get foreign keys for (multi-table mode)'
79
+ }
80
+ },
81
+ required: ['database']
82
+ }
83
+ };
84
+
85
+ export async function handleGetForeignKeys(
86
+ args: unknown,
87
+ dbManager: DatabaseManager
88
+ ): Promise<any> {
89
+ try {
90
+ Logger.info('Executing get_foreign_keys tool');
91
+
92
+ // Validate arguments
93
+ const validationResult = GetForeignKeysArgsSchema.safeParse(args);
94
+ if (!validationResult.success) {
95
+ Logger.warn('Invalid arguments for get_foreign_keys', validationResult.error);
96
+ throw new ToolError(
97
+ `Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
98
+ 'get_foreign_keys'
99
+ );
100
+ }
101
+
102
+ const { database, table, tables } = validationResult.data;
103
+
104
+ // Sanitize inputs
105
+ const sanitizedDatabase = InputValidator.sanitizeString(database);
106
+ const sanitizedTable = table ? InputValidator.sanitizeString(table) : undefined;
107
+ const sanitizedTables = tables ? tables.map(InputValidator.sanitizeString) : undefined;
108
+
109
+ // Validate database name format
110
+ const dbNameValidation = InputValidator.validateDatabaseName(sanitizedDatabase);
111
+ if (!dbNameValidation.isValid) {
112
+ throw new ToolError(
113
+ `Invalid database name: ${dbNameValidation.error}`,
114
+ 'get_foreign_keys'
115
+ );
116
+ }
117
+
118
+ // Multi-table mode
119
+ if (sanitizedTables && sanitizedTables.length > 0) {
120
+ Logger.info(`Multi-table mode: Getting foreign keys for tables: ${sanitizedTables.join(', ')} in database: ${sanitizedDatabase}`);
121
+ const results: Record<string, any> = {};
122
+ const errors: Record<string, string> = {};
123
+
124
+ for (const tbl of sanitizedTables) {
125
+ try {
126
+ const tableNameValidation = InputValidator.validateTableName(tbl);
127
+ if (!tableNameValidation.isValid) {
128
+ throw new ToolError(
129
+ `Invalid table name: ${tableNameValidation.error}`,
130
+ 'get_foreign_keys'
131
+ );
132
+ }
133
+
134
+ Logger.info(`Getting foreign keys for table: ${tbl} in database: ${sanitizedDatabase}`);
135
+ Logger.time(`get_foreign_keys_execution_${tbl}`);
136
+
137
+ const foreignKeys = await dbManager.getForeignKeys(sanitizedDatabase, tbl);
138
+
139
+ Logger.timeEnd(`get_foreign_keys_execution_${tbl}`);
140
+ Logger.info(`Found ${foreignKeys.length} foreign key(s) in table: ${tbl}`);
141
+
142
+ const analysis = analyzeForeignKeyRelationships(foreignKeys);
143
+ const foreignKeysByTable = groupForeignKeysByTable(foreignKeys);
144
+
145
+ results[tbl] = {
146
+ database: sanitizedDatabase,
147
+ table: tbl,
148
+ scope: 'table',
149
+ foreignKeys: foreignKeys.map(fk => ({
150
+ constraintName: fk.constraintName,
151
+ sourceTable: fk.tableName,
152
+ sourceColumn: fk.columnName,
153
+ targetTable: fk.referencedTableName,
154
+ targetColumn: fk.referencedColumnName,
155
+ updateRule: fk.updateRule,
156
+ deleteRule: fk.deleteRule,
157
+ relationshipType: determineRelationshipType(fk.updateRule || 'NO ACTION', fk.deleteRule || 'NO ACTION')
158
+ })),
159
+ relationships: analysis.relationships,
160
+ statistics: {
161
+ totalForeignKeys: foreignKeys.length,
162
+ uniqueConstraints: [...new Set(foreignKeys.map(fk => fk.constraintName))].length,
163
+ affectedTables: [...new Set(foreignKeys.map(fk => fk.tableName))].length,
164
+ referencedTables: [...new Set(foreignKeys.map(fk => fk.referencedTableName))].length,
165
+ cascadeDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').length,
166
+ cascadeUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'CASCADE').length,
167
+ restrictDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT').length,
168
+ restrictUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT').length
169
+ },
170
+ foreignKeysByTable,
171
+ analysis: {
172
+ ...analysis,
173
+ integrityRules: analyzeIntegrityRules(foreignKeys),
174
+ potentialIssues: identifyPotentialIssues(foreignKeys)
175
+ },
176
+ summary: {
177
+ hasRelationships: foreignKeys.length > 0,
178
+ message: foreignKeys.length === 0
179
+ ? `No foreign key relationships found in table '${tbl}' of database '${sanitizedDatabase}'`
180
+ : `Found ${foreignKeys.length} foreign key relationship(s) in table '${tbl}' involving ${analysis.relationships.length} table connection(s)`
181
+ }
182
+ };
183
+
184
+ Logger.debug('get_foreign_keys completed for table', {
185
+ database: sanitizedDatabase,
186
+ table: tbl,
187
+ foreignKeyCount: foreignKeys.length,
188
+ relationshipCount: analysis.relationships.length
189
+ });
190
+
191
+ } catch (err) {
192
+ Logger.error(`Error processing table '${tbl}' in get_foreign_keys`, err);
193
+ errors[tbl] = err instanceof Error ? err.message : String(err);
194
+ }
195
+ }
196
+
197
+ return {
198
+ content: [{
199
+ type: 'text',
200
+ text: JSON.stringify({ database: sanitizedDatabase, results, errors }, null, 2)
201
+ }]
202
+ };
203
+ }
204
+
205
+ // Single-table or all-tables mode (original behavior)
206
+ // Validate table name format if provided
207
+ if (sanitizedTable) {
208
+ const tableNameValidation = InputValidator.validateTableName(sanitizedTable);
209
+ if (!tableNameValidation.isValid) {
210
+ throw new ToolError(
211
+ `Invalid table name: ${tableNameValidation.error}`,
212
+ 'get_foreign_keys'
213
+ );
214
+ }
215
+ }
216
+
217
+ const scope = sanitizedTable ? `table: ${sanitizedTable}` : 'entire database';
218
+ Logger.info(`Getting foreign keys for ${scope} in database: ${sanitizedDatabase}`);
219
+ Logger.time('get_foreign_keys_execution');
220
+
221
+ // Get foreign keys
222
+ const foreignKeys = await dbManager.getForeignKeys(sanitizedDatabase, sanitizedTable);
223
+
224
+ Logger.timeEnd('get_foreign_keys_execution');
225
+ Logger.info(`Found ${foreignKeys.length} foreign key(s) in ${scope}`);
226
+
227
+ // Analyze relationships
228
+ const analysis = analyzeForeignKeyRelationships(foreignKeys);
229
+
230
+ // Group foreign keys by table
231
+ const foreignKeysByTable = groupForeignKeysByTable(foreignKeys);
232
+
233
+ // Create response
234
+ const response = {
235
+ database: sanitizedDatabase,
236
+ table: sanitizedTable || null,
237
+ scope: sanitizedTable ? 'table' : 'database',
238
+ foreignKeys: foreignKeys.map(fk => ({
239
+ constraintName: fk.constraintName,
240
+ sourceTable: fk.tableName,
241
+ sourceColumn: fk.columnName,
242
+ targetTable: fk.referencedTableName,
243
+ targetColumn: fk.referencedColumnName,
244
+ updateRule: fk.updateRule,
245
+ deleteRule: fk.deleteRule,
246
+ relationshipType: determineRelationshipType(fk.updateRule || 'NO ACTION', fk.deleteRule || 'NO ACTION')
247
+ })),
248
+ relationships: analysis.relationships,
249
+ statistics: {
250
+ totalForeignKeys: foreignKeys.length,
251
+ uniqueConstraints: [...new Set(foreignKeys.map(fk => fk.constraintName))].length,
252
+ affectedTables: [...new Set(foreignKeys.map(fk => fk.tableName))].length,
253
+ referencedTables: [...new Set(foreignKeys.map(fk => fk.referencedTableName))].length,
254
+ cascadeDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').length,
255
+ cascadeUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'CASCADE').length,
256
+ restrictDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT').length,
257
+ restrictUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT').length
258
+ },
259
+ foreignKeysByTable,
260
+ analysis: {
261
+ ...analysis,
262
+ integrityRules: analyzeIntegrityRules(foreignKeys),
263
+ potentialIssues: identifyPotentialIssues(foreignKeys)
264
+ },
265
+ summary: {
266
+ hasRelationships: foreignKeys.length > 0,
267
+ message: foreignKeys.length === 0
268
+ ? `No foreign key relationships found in ${scope} of database '${sanitizedDatabase}'`
269
+ : `Found ${foreignKeys.length} foreign key relationship(s) in ${scope} involving ${analysis.relationships.length} table connection(s)`
270
+ }
271
+ };
272
+
273
+ Logger.debug('get_foreign_keys completed successfully', {
274
+ database: sanitizedDatabase,
275
+ table: sanitizedTable,
276
+ foreignKeyCount: foreignKeys.length,
277
+ relationshipCount: analysis.relationships.length
278
+ });
279
+
280
+ return {
281
+ content: [{
282
+ type: 'text',
283
+ text: JSON.stringify(response, null, 2)
284
+ }]
285
+ };
286
+
287
+ } catch (error) {
288
+ // Add table context to error logs
289
+ let tableContext: string | undefined;
290
+ let argTables: string[] | undefined;
291
+ let argTable: string | undefined;
292
+ if (args && typeof args === 'object') {
293
+ // @ts-ignore
294
+ argTables = Array.isArray(args.tables) ? args.tables : undefined;
295
+ // @ts-ignore
296
+ argTable = typeof args.table === 'string' ? args.table : undefined;
297
+ }
298
+ if (Array.isArray(argTables) && argTables.length > 0) {
299
+ tableContext = `tables: [${argTables.join(', ')}]`;
300
+ } else if (typeof argTable === 'string' && argTable.length > 0) {
301
+ tableContext = `table: ${argTable}`;
302
+ } else {
303
+ tableContext = 'no table(s) specified';
304
+ }
305
+ Logger.error(`Error in get_foreign_keys tool (${tableContext})`, error);
306
+
307
+ if (error instanceof ToolError) {
308
+ throw error;
309
+ }
310
+
311
+ throw new ToolError(
312
+ `Failed to get foreign keys (${tableContext}): ${error instanceof Error ? error.message : 'Unknown error'}`,
313
+ 'get_foreign_keys',
314
+ error instanceof Error ? error : undefined
315
+ );
316
+ }
317
+ }
318
+
319
+ // Helper functions
320
+ function groupForeignKeysByTable(foreignKeys: any[]): Record<string, any[]> {
321
+ const groups: Record<string, any[]> = {};
322
+
323
+ foreignKeys.forEach(fk => {
324
+ if (!groups[fk.tableName]) {
325
+ groups[fk.tableName] = [];
326
+ }
327
+ groups[fk.tableName].push({
328
+ constraintName: fk.constraintName,
329
+ column: fk.columnName,
330
+ referencedTable: fk.referencedTableName,
331
+ referencedColumn: fk.referencedColumnName,
332
+ updateRule: fk.updateRule,
333
+ deleteRule: fk.deleteRule
334
+ });
335
+ });
336
+
337
+ return groups;
338
+ }
339
+
340
+ function analyzeForeignKeyRelationships(foreignKeys: any[]): any {
341
+ const relationships: any[] = [];
342
+ const tableConnections: Record<string, Set<string>> = {};
343
+
344
+ // Group by constraint to handle composite foreign keys
345
+ const constraintGroups: Record<string, any[]> = {};
346
+ foreignKeys.forEach(fk => {
347
+ if (!constraintGroups[fk.constraintName]) {
348
+ constraintGroups[fk.constraintName] = [];
349
+ }
350
+ constraintGroups[fk.constraintName].push(fk);
351
+ });
352
+
353
+ // Analyze each constraint
354
+ Object.keys(constraintGroups).forEach(constraintName => {
355
+ const fks = constraintGroups[constraintName];
356
+ const firstFK = fks[0];
357
+
358
+ relationships.push({
359
+ constraintName,
360
+ fromTable: firstFK.tableName,
361
+ toTable: firstFK.referencedTableName,
362
+ columns: fks.map(fk => ({
363
+ from: fk.columnName,
364
+ to: fk.referencedColumnName
365
+ })),
366
+ isComposite: fks.length > 1,
367
+ updateRule: firstFK.updateRule,
368
+ deleteRule: firstFK.deleteRule,
369
+ relationshipStrength: determineRelationshipStrength(firstFK.updateRule, firstFK.deleteRule)
370
+ });
371
+
372
+ // Track table connections
373
+ if (!tableConnections[firstFK.tableName]) {
374
+ tableConnections[firstFK.tableName] = new Set();
375
+ }
376
+ tableConnections[firstFK.tableName].add(firstFK.referencedTableName);
377
+ });
378
+
379
+ return {
380
+ relationships,
381
+ tableConnections: Object.fromEntries(
382
+ Object.entries(tableConnections).map(([table, connections]) => [
383
+ table,
384
+ Array.from(connections)
385
+ ])
386
+ ),
387
+ totalRelationships: relationships.length,
388
+ compositeRelationships: relationships.filter(r => r.isComposite).length
389
+ };
390
+ }
391
+
392
+ function determineRelationshipType(updateRule: string | undefined, deleteRule: string | undefined): string {
393
+ const ur = updateRule || 'NO ACTION';
394
+ const dr = deleteRule || 'NO ACTION';
395
+
396
+ if (ur === 'CASCADE' || dr === 'CASCADE') {
397
+ return 'strong_dependency'; // Child cannot exist without parent
398
+ } else if (dr === 'RESTRICT' || dr === 'NO ACTION') {
399
+ return 'protective'; // Prevents accidental deletion
400
+ } else if (dr === 'SET NULL') {
401
+ return 'optional_reference'; // Relationship can be broken
402
+ } else {
403
+ return 'unknown';
404
+ }
405
+ }
406
+
407
+ function determineRelationshipStrength(updateRule: string, deleteRule: string): 'strong' | 'medium' | 'weak' {
408
+ if (deleteRule === 'CASCADE' && updateRule === 'CASCADE') {
409
+ return 'strong';
410
+ } else if (deleteRule === 'RESTRICT' || updateRule === 'RESTRICT') {
411
+ return 'medium';
412
+ } else {
413
+ return 'weak';
414
+ }
415
+ }
416
+
417
+ function analyzeIntegrityRules(foreignKeys: any[]): any {
418
+ const rules = {
419
+ cascade: {
420
+ delete: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE'),
421
+ update: foreignKeys.filter(fk => fk.updateRule === 'CASCADE')
422
+ },
423
+ restrict: {
424
+ delete: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT' || fk.deleteRule === 'NO ACTION'),
425
+ update: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT' || fk.updateRule === 'NO ACTION')
426
+ },
427
+ setNull: {
428
+ delete: foreignKeys.filter(fk => fk.deleteRule === 'SET NULL'),
429
+ update: foreignKeys.filter(fk => fk.updateRule === 'SET NULL')
430
+ }
431
+ };
432
+
433
+ return {
434
+ rules,
435
+ summary: {
436
+ cascadeDeleteTables: [...new Set(rules.cascade.delete.map(fk => fk.tableName))],
437
+ protectedTables: [...new Set(rules.restrict.delete.map(fk => fk.referencedTableName))],
438
+ weaklyReferencedTables: [...new Set(rules.setNull.delete.map(fk => fk.referencedTableName))]
439
+ }
440
+ };
441
+ }
442
+
443
+ function identifyPotentialIssues(foreignKeys: any[]): string[] {
444
+ const issues: string[] = [];
445
+
446
+ // Check for circular references
447
+ const tableGraph: Record<string, string[]> = {};
448
+ foreignKeys.forEach(fk => {
449
+ if (!tableGraph[fk.tableName]) {
450
+ tableGraph[fk.tableName] = [];
451
+ }
452
+ tableGraph[fk.tableName].push(fk.referencedTableName);
453
+ });
454
+
455
+ // Simple cycle detection (this is a basic check, not comprehensive)
456
+ Object.keys(tableGraph).forEach(table => {
457
+ tableGraph[table].forEach(referenced => {
458
+ if (tableGraph[referenced] && tableGraph[referenced].includes(table)) {
459
+ issues.push(`Potential circular reference detected between tables '${table}' and '${referenced}'`);
460
+ }
461
+ });
462
+ });
463
+
464
+ // Check for many cascade delete relationships from same parent
465
+ const cascadeParents: Record<string, number> = {};
466
+ foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').forEach(fk => {
467
+ cascadeParents[fk.referencedTableName] = (cascadeParents[fk.referencedTableName] || 0) + 1;
468
+ });
469
+
470
+ Object.entries(cascadeParents).forEach(([table, count]) => {
471
+ if (count > 5) {
472
+ issues.push(`Table '${table}' has ${count} cascade delete relationships - consider the impact of deletions`);
473
+ }
474
+ });
475
+
476
+ // Check for inconsistent naming patterns
477
+ const constraintNames = foreignKeys.map(fk => fk.constraintName);
478
+ const hasConsistentNaming = constraintNames.every(name =>
479
+ name.startsWith('fk_') || name.startsWith('FK_') ||
480
+ name.includes('_fk') || name.includes('_FK')
481
+ );
482
+
483
+ if (!hasConsistentNaming && constraintNames.length > 1) {
484
+ issues.push('Foreign key constraint names follow inconsistent naming patterns');
485
+ }
486
+
487
+ return issues;
488
+ }
@@ -0,0 +1,51 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { handleGetIndexes } from './get-indexes';
4
+ import { DatabaseManager } from '../database/manager';
5
+
6
+ const mockDbManager = {
7
+ getIndexes: vi.fn()
8
+ } as unknown as DatabaseManager;
9
+
10
+ describe('get_indexes', () => {
11
+ it('should return indexes for a single table', async () => {
12
+ mockDbManager.getIndexes.mockResolvedValueOnce([{ indexName: 'PRIMARY', tableName: 'products', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }]);
13
+ const args = { database: 'testdb', table: 'products' };
14
+ const result = await handleGetIndexes(args, mockDbManager);
15
+ const parsed = JSON.parse(result.content[0].text);
16
+ expect(parsed.table).toBe('products');
17
+ expect(parsed.indexes[0].name).toBe('PRIMARY');
18
+ });
19
+
20
+ it('should return indexes for multiple tables', async () => {
21
+ mockDbManager.getIndexes
22
+ .mockResolvedValueOnce([{ indexName: 'PRIMARY', tableName: 'products', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }])
23
+ .mockResolvedValueOnce([{ indexName: 'PRIMARY', tableName: 'categories', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }]);
24
+ const args = { database: 'testdb', tables: ['products', 'categories'] };
25
+ const result = await handleGetIndexes(args, mockDbManager);
26
+ const parsed = JSON.parse(result.content[0].text);
27
+ expect(Object.keys(parsed)).toHaveLength(2);
28
+ expect(parsed.products.table).toBe('products');
29
+ expect(parsed.categories.table).toBe('categories');
30
+ });
31
+
32
+ it('should throw error if neither table nor tables is provided', async () => {
33
+ await expect(() =>
34
+ handleGetIndexes({ database: 'testdb' }, mockDbManager)
35
+ ).rejects.toThrow(/Either 'table' or non-empty 'tables'/);
36
+ });
37
+
38
+ it('should throw error for invalid table name', async () => {
39
+ await expect(() =>
40
+ handleGetIndexes({ database: 'testdb', table: 'invalid;DROP' }, mockDbManager)
41
+ ).rejects.toThrow(/Invalid table name/);
42
+ });
43
+
44
+ it('should return empty result if table not found', async () => {
45
+ mockDbManager.getIndexes.mockResolvedValueOnce([]);
46
+ const result = await handleGetIndexes({ database: 'testdb', table: 'notfound' }, mockDbManager);
47
+ const parsed = JSON.parse(result.content[0].text);
48
+ expect(parsed.indexes).toHaveLength(0);
49
+ expect(parsed.summary.message).toMatch(/No indexes found/);
50
+ });
51
+ });