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.
- package/README.md +197 -0
- package/dist/database/connection.d.ts +13 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +155 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/manager.d.ts +28 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +621 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/postgres-connection.d.ts +10 -0
- package/dist/database/postgres-connection.d.ts.map +1 -0
- package/dist/database/postgres-connection.js +113 -0
- package/dist/database/postgres-connection.js.map +1 -0
- package/dist/database/types.d.ts +84 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +6 -0
- package/dist/database/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +186 -0
- package/dist/server.js.map +1 -0
- package/dist/test-defaults.d.ts +2 -0
- package/dist/test-defaults.d.ts.map +1 -0
- package/dist/test-defaults.js +57 -0
- package/dist/test-defaults.js.map +1 -0
- package/dist/tools/analyze-query.d.ts +27 -0
- package/dist/tools/analyze-query.d.ts.map +1 -0
- package/dist/tools/analyze-query.js +71 -0
- package/dist/tools/analyze-query.js.map +1 -0
- package/dist/tools/execute-query.d.ts +33 -0
- package/dist/tools/execute-query.d.ts.map +1 -0
- package/dist/tools/execute-query.js +57 -0
- package/dist/tools/execute-query.js.map +1 -0
- package/dist/tools/get-foreign-keys.d.ts +38 -0
- package/dist/tools/get-foreign-keys.d.ts.map +1 -0
- package/dist/tools/get-foreign-keys.js +391 -0
- package/dist/tools/get-foreign-keys.js.map +1 -0
- package/dist/tools/get-indexes.d.ts +38 -0
- package/dist/tools/get-indexes.d.ts.map +1 -0
- package/dist/tools/get-indexes.js +472 -0
- package/dist/tools/get-indexes.js.map +1 -0
- package/dist/tools/information-schema-query.d.ts +33 -0
- package/dist/tools/information-schema-query.d.ts.map +1 -0
- package/dist/tools/information-schema-query.js +76 -0
- package/dist/tools/information-schema-query.js.map +1 -0
- package/dist/tools/inspect-table.d.ts +38 -0
- package/dist/tools/inspect-table.d.ts.map +1 -0
- package/dist/tools/inspect-table.js +351 -0
- package/dist/tools/inspect-table.js.map +1 -0
- package/dist/tools/list-databases.d.ts +14 -0
- package/dist/tools/list-databases.d.ts.map +1 -0
- package/dist/tools/list-databases.js +83 -0
- package/dist/tools/list-databases.js.map +1 -0
- package/dist/tools/list-tables.d.ts +19 -0
- package/dist/tools/list-tables.d.ts.map +1 -0
- package/dist/tools/list-tables.js +130 -0
- package/dist/tools/list-tables.js.map +1 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +98 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/validators/input-validator.d.ts +76 -0
- package/dist/validators/input-validator.d.ts.map +1 -0
- package/dist/validators/input-validator.js +295 -0
- package/dist/validators/input-validator.js.map +1 -0
- package/dist/validators/query-validator.d.ts +19 -0
- package/dist/validators/query-validator.d.ts.map +1 -0
- package/dist/validators/query-validator.js +229 -0
- package/dist/validators/query-validator.js.map +1 -0
- package/enhanced_sql_prompt.md +324 -0
- package/examples/claude-config.json +23 -0
- package/examples/roo-config.json +16 -0
- package/package.json +42 -0
- package/src/database/connection.ts +165 -0
- package/src/database/manager.ts +682 -0
- package/src/database/postgres-connection.ts +123 -0
- package/src/database/types.ts +93 -0
- package/src/index.ts +136 -0
- package/src/server.ts +254 -0
- package/src/test-defaults.ts +63 -0
- package/src/tools/analyze-query.test.ts +100 -0
- package/src/tools/analyze-query.ts +112 -0
- package/src/tools/execute-query.ts +91 -0
- package/src/tools/get-foreign-keys.test.ts +51 -0
- package/src/tools/get-foreign-keys.ts +488 -0
- package/src/tools/get-indexes.test.ts +51 -0
- package/src/tools/get-indexes.ts +570 -0
- package/src/tools/information-schema-query.ts +125 -0
- package/src/tools/inspect-table.test.ts +59 -0
- package/src/tools/inspect-table.ts +440 -0
- package/src/tools/list-databases.ts +119 -0
- package/src/tools/list-tables.ts +181 -0
- package/src/utils/errors.ts +103 -0
- package/src/utils/logger.ts +158 -0
- package/src/validators/input-validator.ts +318 -0
- package/src/validators/query-validator.ts +267 -0
- 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
|
+
});
|