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,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { DatabaseManager } from '../database/manager.js';
|
|
3
|
+
export declare const AnalyzeQueryArgsSchema: z.ZodObject<{
|
|
4
|
+
database: z.ZodString;
|
|
5
|
+
query: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export interface AnalyzeQueryTool {
|
|
8
|
+
name: 'analyze_query';
|
|
9
|
+
description: 'Analyze a SQL query performance and provide recommendations';
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object';
|
|
12
|
+
properties: {
|
|
13
|
+
database: {
|
|
14
|
+
type: 'string';
|
|
15
|
+
description: 'Name of the database to run the analysis against';
|
|
16
|
+
};
|
|
17
|
+
query: {
|
|
18
|
+
type: 'string';
|
|
19
|
+
description: 'The SQL query to analyze';
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: ['database', 'query'];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const analyzeQueryToolDefinition: AnalyzeQueryTool;
|
|
26
|
+
export declare function handleAnalyzeQuery(args: unknown, dbManager: DatabaseManager): Promise<any>;
|
|
27
|
+
//# sourceMappingURL=analyze-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-query.d.ts","sourceRoot":"","sources":["../../src/tools/analyze-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAMzD,eAAO,MAAM,sBAAsB;;;iBAGjC,CAAC;AAEH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,WAAW,EAAE,6DAA6D,CAAC;IAC3E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ,CAAC;gBACf,WAAW,EAAE,kDAAkD,CAAC;aACjE,CAAC;YACF,KAAK,EAAE;gBACL,IAAI,EAAE,QAAQ,CAAC;gBACf,WAAW,EAAE,0BAA0B,CAAC;aACzC,CAAC;SACH,CAAC;QACF,QAAQ,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAiBxC,CAAC;AAEF,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC,GAAG,CAAC,CA0Dd"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InputValidator } from '../validators/input-validator.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { ToolError } from '../utils/errors.js';
|
|
5
|
+
// Tool schema
|
|
6
|
+
export const AnalyzeQueryArgsSchema = z.object({
|
|
7
|
+
database: z.string().min(1, 'Database name is required'),
|
|
8
|
+
query: z.string().min(1, 'SQL query is required')
|
|
9
|
+
});
|
|
10
|
+
export const analyzeQueryToolDefinition = {
|
|
11
|
+
name: 'analyze_query',
|
|
12
|
+
description: 'Analyze a SQL query performance and provide recommendations',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
database: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Name of the database to run the analysis against'
|
|
19
|
+
},
|
|
20
|
+
query: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'The SQL query to analyze'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
required: ['database', 'query']
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export async function handleAnalyzeQuery(args, dbManager) {
|
|
29
|
+
try {
|
|
30
|
+
Logger.info('Executing analyze_query tool');
|
|
31
|
+
// Validate arguments
|
|
32
|
+
const validationResult = AnalyzeQueryArgsSchema.safeParse(args);
|
|
33
|
+
if (!validationResult.success) {
|
|
34
|
+
Logger.warn('Invalid arguments for analyze_query', validationResult.error);
|
|
35
|
+
throw new ToolError(`Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`, 'analyze_query');
|
|
36
|
+
}
|
|
37
|
+
const { database, query } = validationResult.data;
|
|
38
|
+
// Sanitize database name
|
|
39
|
+
const sanitizedDatabase = InputValidator.sanitizeString(database);
|
|
40
|
+
Logger.info(`Analyzing query for database: ${sanitizedDatabase}`);
|
|
41
|
+
const analysis = await dbManager.analyzeQuery(sanitizedDatabase, query);
|
|
42
|
+
const response = {
|
|
43
|
+
database: sanitizedDatabase,
|
|
44
|
+
query: analysis.query,
|
|
45
|
+
type: analysis.type,
|
|
46
|
+
summary: {
|
|
47
|
+
cost: analysis.summary.cost,
|
|
48
|
+
operations: analysis.summary.operations,
|
|
49
|
+
potentialIssues: analysis.summary.potentialIssues,
|
|
50
|
+
recommendation: analysis.summary.potentialIssues.length > 0
|
|
51
|
+
? `Found ${analysis.summary.potentialIssues.length} potential performance issues. Consider adding indexes or refactoring the query.`
|
|
52
|
+
: 'No major performance issues detected in the execution plan.'
|
|
53
|
+
},
|
|
54
|
+
executionPlan: analysis.plan
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
content: [{
|
|
58
|
+
type: 'text',
|
|
59
|
+
text: JSON.stringify(response, null, 2)
|
|
60
|
+
}]
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
Logger.error('Error in analyze_query tool', error);
|
|
65
|
+
if (error instanceof ToolError) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
throw new ToolError(`Failed to analyze query: ${error instanceof Error ? error.message : 'Unknown error'}`, 'analyze_query', error instanceof Error ? error : undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=analyze-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-query.js","sourceRoot":"","sources":["../../src/tools/analyze-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DatabaseManager } from '../database/manager.js';
|
|
2
|
+
export interface ExecuteQueryTool {
|
|
3
|
+
name: 'execute_query';
|
|
4
|
+
description: 'Execute a safe read-only SQL query (SELECT, SHOW, DESCRIBE, EXPLAIN). Automatic row limits are applied for security.';
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: 'object';
|
|
7
|
+
properties: {
|
|
8
|
+
database: {
|
|
9
|
+
type: 'string';
|
|
10
|
+
description: 'Database name';
|
|
11
|
+
};
|
|
12
|
+
query: {
|
|
13
|
+
type: 'string';
|
|
14
|
+
description: 'SQL query to execute';
|
|
15
|
+
};
|
|
16
|
+
params?: {
|
|
17
|
+
type: 'array';
|
|
18
|
+
items: {
|
|
19
|
+
type: 'any';
|
|
20
|
+
};
|
|
21
|
+
description: 'Optional parameters for the query';
|
|
22
|
+
};
|
|
23
|
+
limit?: {
|
|
24
|
+
type: 'number';
|
|
25
|
+
description: 'Optional limit for number of rows (default 1000, max 1000)';
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
required: ['database', 'query'];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare const executeQueryToolDefinition: ExecuteQueryTool;
|
|
32
|
+
export declare function handleExecuteQuery(args: unknown, dbManager: DatabaseManager): Promise<any>;
|
|
33
|
+
//# sourceMappingURL=execute-query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-query.d.ts","sourceRoot":"","sources":["../../src/tools/execute-query.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAazD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,eAAe,CAAC;IACtB,WAAW,EAAE,sHAAsH,CAAC;IACpI,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE;YACV,QAAQ,EAAE;gBAAE,IAAI,EAAE,QAAQ,CAAC;gBAAC,WAAW,EAAE,eAAe,CAAA;aAAE,CAAC;YAC3D,KAAK,EAAE;gBAAE,IAAI,EAAE,QAAQ,CAAC;gBAAC,WAAW,EAAE,sBAAsB,CAAA;aAAE,CAAC;YAC/D,MAAM,CAAC,EAAE;gBAAE,IAAI,EAAE,OAAO,CAAC;gBAAC,KAAK,EAAE;oBAAE,IAAI,EAAE,KAAK,CAAA;iBAAE,CAAC;gBAAC,WAAW,EAAE,mCAAmC,CAAA;aAAE,CAAC;YACrG,KAAK,CAAC,EAAE;gBAAE,IAAI,EAAE,QAAQ,CAAC;gBAAC,WAAW,EAAE,4DAA4D,CAAA;aAAE,CAAC;SACvG,CAAC;QACF,QAAQ,EAAE,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;KACjC,CAAC;CACH;AAED,eAAO,MAAM,0BAA0B,EAAE,gBAaxC,CAAC;AAEF,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC,GAAG,CAAC,CA2Cd"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InputValidator } from '../validators/input-validator.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { ToolError } from '../utils/errors.js';
|
|
5
|
+
// Zod schema for arguments
|
|
6
|
+
const ExecuteQueryArgsSchema = z.object({
|
|
7
|
+
database: z.string().min(1, 'Database name is required'),
|
|
8
|
+
query: z.string().min(1, 'SQL query is required'),
|
|
9
|
+
params: z.array(z.any()).optional(),
|
|
10
|
+
limit: z.number().int().min(1).max(1000).optional()
|
|
11
|
+
});
|
|
12
|
+
export const executeQueryToolDefinition = {
|
|
13
|
+
name: 'execute_query',
|
|
14
|
+
description: 'Execute a safe read-only SQL query (SELECT, SHOW, DESCRIBE, EXPLAIN). Automatic row limits are applied for security.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
database: { type: 'string', description: 'Database name' },
|
|
19
|
+
query: { type: 'string', description: 'SQL query to execute' },
|
|
20
|
+
params: { type: 'array', items: { type: 'any' }, description: 'Optional parameters for the query' },
|
|
21
|
+
limit: { type: 'number', description: 'Optional limit for number of rows (default 1000, max 1000)' }
|
|
22
|
+
},
|
|
23
|
+
required: ['database', 'query']
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export async function handleExecuteQuery(args, dbManager) {
|
|
27
|
+
try {
|
|
28
|
+
Logger.info('Executing execute_query tool');
|
|
29
|
+
const validationResult = ExecuteQueryArgsSchema.safeParse(args);
|
|
30
|
+
if (!validationResult.success) {
|
|
31
|
+
Logger.warn('Invalid arguments for execute_query', validationResult.error);
|
|
32
|
+
throw new ToolError(`Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`, 'execute_query');
|
|
33
|
+
}
|
|
34
|
+
const { database, query, params, limit } = validationResult.data;
|
|
35
|
+
// Sanitize and validate database name
|
|
36
|
+
const sanitizedDatabase = InputValidator.sanitizeString(database);
|
|
37
|
+
const dbNameValidation = InputValidator.validateDatabaseName(sanitizedDatabase);
|
|
38
|
+
if (!dbNameValidation.isValid) {
|
|
39
|
+
throw new ToolError(`Invalid database name: ${dbNameValidation.error}`, 'execute_query');
|
|
40
|
+
}
|
|
41
|
+
Logger.info(`Executing query on database ${sanitizedDatabase}`);
|
|
42
|
+
const result = await dbManager.executeQuery(sanitizedDatabase, query, params);
|
|
43
|
+
return {
|
|
44
|
+
content: [{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: JSON.stringify(result.rows, null, 2)
|
|
47
|
+
}]
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
Logger.error('Error in execute_query tool', error);
|
|
52
|
+
if (error instanceof ToolError)
|
|
53
|
+
throw error;
|
|
54
|
+
throw new ToolError(`Failed to execute query: ${error instanceof Error ? error.message : 'Unknown error'}`, 'execute_query', error instanceof Error ? error : undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=execute-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-query.js","sourceRoot":"","sources":["../../src/tools/execute-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DatabaseManager } from '../database/manager.js';
|
|
2
|
+
/**
|
|
3
|
+
* Tool: get_foreign_keys
|
|
4
|
+
* Get foreign key relationships for one or more tables, or the entire database.
|
|
5
|
+
*
|
|
6
|
+
* Supports both single-table and multi-table inspection:
|
|
7
|
+
* - Provide either `table` (string) for a single table, or `tables` (string[]) for multiple tables.
|
|
8
|
+
* - If `tables` is provided, returns a mapping of table names to their foreign key analysis.
|
|
9
|
+
* - Do not provide both `table` and `tables` at the same time.
|
|
10
|
+
*/
|
|
11
|
+
export interface GetForeignKeysTool {
|
|
12
|
+
name: 'get_foreign_keys';
|
|
13
|
+
description: 'Get foreign key relationships for one or more tables, or the entire database. Supports multi-table inspection via the tables: string[] parameter.';
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object';
|
|
16
|
+
properties: {
|
|
17
|
+
database: {
|
|
18
|
+
type: 'string';
|
|
19
|
+
description: 'Name of the database to analyze';
|
|
20
|
+
};
|
|
21
|
+
table: {
|
|
22
|
+
type: 'string';
|
|
23
|
+
description: 'Specific table name to get foreign keys for (single-table mode)';
|
|
24
|
+
};
|
|
25
|
+
tables: {
|
|
26
|
+
type: 'array';
|
|
27
|
+
items: {
|
|
28
|
+
type: 'string';
|
|
29
|
+
};
|
|
30
|
+
description: 'Array of table names to get foreign keys for (multi-table mode)';
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
required: ['database'];
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare const getForeignKeysToolDefinition: GetForeignKeysTool;
|
|
37
|
+
export declare function handleGetForeignKeys(args: unknown, dbManager: DatabaseManager): Promise<any>;
|
|
38
|
+
//# sourceMappingURL=get-foreign-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-foreign-keys.d.ts","sourceRoot":"","sources":["../../src/tools/get-foreign-keys.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA0BzD;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,EAAE,mJAAmJ,CAAC;IACjK,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ,CAAC;gBACf,WAAW,EAAE,iCAAiC,CAAC;aAChD,CAAC;YACF,KAAK,EAAE;gBACL,IAAI,EAAE,QAAQ,CAAC;gBACf,WAAW,EAAE,iEAAiE,CAAC;aAChF,CAAC;YACF,MAAM,EAAE;gBACN,IAAI,EAAE,OAAO,CAAC;gBACd,KAAK,EAAE;oBAAE,IAAI,EAAE,QAAQ,CAAA;iBAAE,CAAC;gBAC1B,WAAW,EAAE,iEAAiE,CAAC;aAChF,CAAC;SACH,CAAC;QACF,QAAQ,EAAE,CAAC,UAAU,CAAC,CAAC;KACxB,CAAC;CACH;AAED,eAAO,MAAM,4BAA4B,EAAE,kBAsB1C,CAAC;AAEF,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,eAAe,GACzB,OAAO,CAAC,GAAG,CAAC,CAqOd"}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { InputValidator } from '../validators/input-validator.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { ToolError } from '../utils/errors.js';
|
|
5
|
+
// Tool schema
|
|
6
|
+
const GetForeignKeysArgsSchema = z.object({
|
|
7
|
+
database: z.string().min(1, 'Database name is required'),
|
|
8
|
+
table: z.string().optional(),
|
|
9
|
+
tables: z.array(z.string().min(1)).optional()
|
|
10
|
+
}).superRefine((data, ctx) => {
|
|
11
|
+
const hasTable = typeof data.table === 'string' && data.table.length > 0;
|
|
12
|
+
const hasTables = Array.isArray(data.tables) && data.tables.length > 0;
|
|
13
|
+
if (hasTable && hasTables) {
|
|
14
|
+
ctx.addIssue({
|
|
15
|
+
code: z.ZodIssueCode.custom,
|
|
16
|
+
message: "Provide either 'table' or 'tables', not both"
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
else if (!hasTable && !hasTables) {
|
|
20
|
+
ctx.addIssue({
|
|
21
|
+
code: z.ZodIssueCode.custom,
|
|
22
|
+
message: "Either 'table' or non-empty 'tables' must be provided"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
export const getForeignKeysToolDefinition = {
|
|
27
|
+
name: 'get_foreign_keys',
|
|
28
|
+
description: 'Get foreign key relationships for one or more tables, or the entire database. Supports multi-table inspection via the tables: string[] parameter.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
database: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Name of the database to analyze'
|
|
35
|
+
},
|
|
36
|
+
table: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Specific table name to get foreign keys for (single-table mode)'
|
|
39
|
+
},
|
|
40
|
+
tables: {
|
|
41
|
+
type: 'array',
|
|
42
|
+
items: { type: 'string' },
|
|
43
|
+
description: 'Array of table names to get foreign keys for (multi-table mode)'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
required: ['database']
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export async function handleGetForeignKeys(args, dbManager) {
|
|
50
|
+
try {
|
|
51
|
+
Logger.info('Executing get_foreign_keys tool');
|
|
52
|
+
// Validate arguments
|
|
53
|
+
const validationResult = GetForeignKeysArgsSchema.safeParse(args);
|
|
54
|
+
if (!validationResult.success) {
|
|
55
|
+
Logger.warn('Invalid arguments for get_foreign_keys', validationResult.error);
|
|
56
|
+
throw new ToolError(`Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`, 'get_foreign_keys');
|
|
57
|
+
}
|
|
58
|
+
const { database, table, tables } = validationResult.data;
|
|
59
|
+
// Sanitize inputs
|
|
60
|
+
const sanitizedDatabase = InputValidator.sanitizeString(database);
|
|
61
|
+
const sanitizedTable = table ? InputValidator.sanitizeString(table) : undefined;
|
|
62
|
+
const sanitizedTables = tables ? tables.map(InputValidator.sanitizeString) : undefined;
|
|
63
|
+
// Validate database name format
|
|
64
|
+
const dbNameValidation = InputValidator.validateDatabaseName(sanitizedDatabase);
|
|
65
|
+
if (!dbNameValidation.isValid) {
|
|
66
|
+
throw new ToolError(`Invalid database name: ${dbNameValidation.error}`, 'get_foreign_keys');
|
|
67
|
+
}
|
|
68
|
+
// Multi-table mode
|
|
69
|
+
if (sanitizedTables && sanitizedTables.length > 0) {
|
|
70
|
+
Logger.info(`Multi-table mode: Getting foreign keys for tables: ${sanitizedTables.join(', ')} in database: ${sanitizedDatabase}`);
|
|
71
|
+
const results = {};
|
|
72
|
+
const errors = {};
|
|
73
|
+
for (const tbl of sanitizedTables) {
|
|
74
|
+
try {
|
|
75
|
+
const tableNameValidation = InputValidator.validateTableName(tbl);
|
|
76
|
+
if (!tableNameValidation.isValid) {
|
|
77
|
+
throw new ToolError(`Invalid table name: ${tableNameValidation.error}`, 'get_foreign_keys');
|
|
78
|
+
}
|
|
79
|
+
Logger.info(`Getting foreign keys for table: ${tbl} in database: ${sanitizedDatabase}`);
|
|
80
|
+
Logger.time(`get_foreign_keys_execution_${tbl}`);
|
|
81
|
+
const foreignKeys = await dbManager.getForeignKeys(sanitizedDatabase, tbl);
|
|
82
|
+
Logger.timeEnd(`get_foreign_keys_execution_${tbl}`);
|
|
83
|
+
Logger.info(`Found ${foreignKeys.length} foreign key(s) in table: ${tbl}`);
|
|
84
|
+
const analysis = analyzeForeignKeyRelationships(foreignKeys);
|
|
85
|
+
const foreignKeysByTable = groupForeignKeysByTable(foreignKeys);
|
|
86
|
+
results[tbl] = {
|
|
87
|
+
database: sanitizedDatabase,
|
|
88
|
+
table: tbl,
|
|
89
|
+
scope: 'table',
|
|
90
|
+
foreignKeys: foreignKeys.map(fk => ({
|
|
91
|
+
constraintName: fk.constraintName,
|
|
92
|
+
sourceTable: fk.tableName,
|
|
93
|
+
sourceColumn: fk.columnName,
|
|
94
|
+
targetTable: fk.referencedTableName,
|
|
95
|
+
targetColumn: fk.referencedColumnName,
|
|
96
|
+
updateRule: fk.updateRule,
|
|
97
|
+
deleteRule: fk.deleteRule,
|
|
98
|
+
relationshipType: determineRelationshipType(fk.updateRule || 'NO ACTION', fk.deleteRule || 'NO ACTION')
|
|
99
|
+
})),
|
|
100
|
+
relationships: analysis.relationships,
|
|
101
|
+
statistics: {
|
|
102
|
+
totalForeignKeys: foreignKeys.length,
|
|
103
|
+
uniqueConstraints: [...new Set(foreignKeys.map(fk => fk.constraintName))].length,
|
|
104
|
+
affectedTables: [...new Set(foreignKeys.map(fk => fk.tableName))].length,
|
|
105
|
+
referencedTables: [...new Set(foreignKeys.map(fk => fk.referencedTableName))].length,
|
|
106
|
+
cascadeDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').length,
|
|
107
|
+
cascadeUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'CASCADE').length,
|
|
108
|
+
restrictDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT').length,
|
|
109
|
+
restrictUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT').length
|
|
110
|
+
},
|
|
111
|
+
foreignKeysByTable,
|
|
112
|
+
analysis: {
|
|
113
|
+
...analysis,
|
|
114
|
+
integrityRules: analyzeIntegrityRules(foreignKeys),
|
|
115
|
+
potentialIssues: identifyPotentialIssues(foreignKeys)
|
|
116
|
+
},
|
|
117
|
+
summary: {
|
|
118
|
+
hasRelationships: foreignKeys.length > 0,
|
|
119
|
+
message: foreignKeys.length === 0
|
|
120
|
+
? `No foreign key relationships found in table '${tbl}' of database '${sanitizedDatabase}'`
|
|
121
|
+
: `Found ${foreignKeys.length} foreign key relationship(s) in table '${tbl}' involving ${analysis.relationships.length} table connection(s)`
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
Logger.debug('get_foreign_keys completed for table', {
|
|
125
|
+
database: sanitizedDatabase,
|
|
126
|
+
table: tbl,
|
|
127
|
+
foreignKeyCount: foreignKeys.length,
|
|
128
|
+
relationshipCount: analysis.relationships.length
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
Logger.error(`Error processing table '${tbl}' in get_foreign_keys`, err);
|
|
133
|
+
errors[tbl] = err instanceof Error ? err.message : String(err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: JSON.stringify({ database: sanitizedDatabase, results, errors }, null, 2)
|
|
140
|
+
}]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
// Single-table or all-tables mode (original behavior)
|
|
144
|
+
// Validate table name format if provided
|
|
145
|
+
if (sanitizedTable) {
|
|
146
|
+
const tableNameValidation = InputValidator.validateTableName(sanitizedTable);
|
|
147
|
+
if (!tableNameValidation.isValid) {
|
|
148
|
+
throw new ToolError(`Invalid table name: ${tableNameValidation.error}`, 'get_foreign_keys');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const scope = sanitizedTable ? `table: ${sanitizedTable}` : 'entire database';
|
|
152
|
+
Logger.info(`Getting foreign keys for ${scope} in database: ${sanitizedDatabase}`);
|
|
153
|
+
Logger.time('get_foreign_keys_execution');
|
|
154
|
+
// Get foreign keys
|
|
155
|
+
const foreignKeys = await dbManager.getForeignKeys(sanitizedDatabase, sanitizedTable);
|
|
156
|
+
Logger.timeEnd('get_foreign_keys_execution');
|
|
157
|
+
Logger.info(`Found ${foreignKeys.length} foreign key(s) in ${scope}`);
|
|
158
|
+
// Analyze relationships
|
|
159
|
+
const analysis = analyzeForeignKeyRelationships(foreignKeys);
|
|
160
|
+
// Group foreign keys by table
|
|
161
|
+
const foreignKeysByTable = groupForeignKeysByTable(foreignKeys);
|
|
162
|
+
// Create response
|
|
163
|
+
const response = {
|
|
164
|
+
database: sanitizedDatabase,
|
|
165
|
+
table: sanitizedTable || null,
|
|
166
|
+
scope: sanitizedTable ? 'table' : 'database',
|
|
167
|
+
foreignKeys: foreignKeys.map(fk => ({
|
|
168
|
+
constraintName: fk.constraintName,
|
|
169
|
+
sourceTable: fk.tableName,
|
|
170
|
+
sourceColumn: fk.columnName,
|
|
171
|
+
targetTable: fk.referencedTableName,
|
|
172
|
+
targetColumn: fk.referencedColumnName,
|
|
173
|
+
updateRule: fk.updateRule,
|
|
174
|
+
deleteRule: fk.deleteRule,
|
|
175
|
+
relationshipType: determineRelationshipType(fk.updateRule || 'NO ACTION', fk.deleteRule || 'NO ACTION')
|
|
176
|
+
})),
|
|
177
|
+
relationships: analysis.relationships,
|
|
178
|
+
statistics: {
|
|
179
|
+
totalForeignKeys: foreignKeys.length,
|
|
180
|
+
uniqueConstraints: [...new Set(foreignKeys.map(fk => fk.constraintName))].length,
|
|
181
|
+
affectedTables: [...new Set(foreignKeys.map(fk => fk.tableName))].length,
|
|
182
|
+
referencedTables: [...new Set(foreignKeys.map(fk => fk.referencedTableName))].length,
|
|
183
|
+
cascadeDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').length,
|
|
184
|
+
cascadeUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'CASCADE').length,
|
|
185
|
+
restrictDeleteCount: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT').length,
|
|
186
|
+
restrictUpdateCount: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT').length
|
|
187
|
+
},
|
|
188
|
+
foreignKeysByTable,
|
|
189
|
+
analysis: {
|
|
190
|
+
...analysis,
|
|
191
|
+
integrityRules: analyzeIntegrityRules(foreignKeys),
|
|
192
|
+
potentialIssues: identifyPotentialIssues(foreignKeys)
|
|
193
|
+
},
|
|
194
|
+
summary: {
|
|
195
|
+
hasRelationships: foreignKeys.length > 0,
|
|
196
|
+
message: foreignKeys.length === 0
|
|
197
|
+
? `No foreign key relationships found in ${scope} of database '${sanitizedDatabase}'`
|
|
198
|
+
: `Found ${foreignKeys.length} foreign key relationship(s) in ${scope} involving ${analysis.relationships.length} table connection(s)`
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
Logger.debug('get_foreign_keys completed successfully', {
|
|
202
|
+
database: sanitizedDatabase,
|
|
203
|
+
table: sanitizedTable,
|
|
204
|
+
foreignKeyCount: foreignKeys.length,
|
|
205
|
+
relationshipCount: analysis.relationships.length
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
content: [{
|
|
209
|
+
type: 'text',
|
|
210
|
+
text: JSON.stringify(response, null, 2)
|
|
211
|
+
}]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// Add table context to error logs
|
|
216
|
+
let tableContext;
|
|
217
|
+
let argTables;
|
|
218
|
+
let argTable;
|
|
219
|
+
if (args && typeof args === 'object') {
|
|
220
|
+
// @ts-ignore
|
|
221
|
+
argTables = Array.isArray(args.tables) ? args.tables : undefined;
|
|
222
|
+
// @ts-ignore
|
|
223
|
+
argTable = typeof args.table === 'string' ? args.table : undefined;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(argTables) && argTables.length > 0) {
|
|
226
|
+
tableContext = `tables: [${argTables.join(', ')}]`;
|
|
227
|
+
}
|
|
228
|
+
else if (typeof argTable === 'string' && argTable.length > 0) {
|
|
229
|
+
tableContext = `table: ${argTable}`;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
tableContext = 'no table(s) specified';
|
|
233
|
+
}
|
|
234
|
+
Logger.error(`Error in get_foreign_keys tool (${tableContext})`, error);
|
|
235
|
+
if (error instanceof ToolError) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
throw new ToolError(`Failed to get foreign keys (${tableContext}): ${error instanceof Error ? error.message : 'Unknown error'}`, 'get_foreign_keys', error instanceof Error ? error : undefined);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Helper functions
|
|
242
|
+
function groupForeignKeysByTable(foreignKeys) {
|
|
243
|
+
const groups = {};
|
|
244
|
+
foreignKeys.forEach(fk => {
|
|
245
|
+
if (!groups[fk.tableName]) {
|
|
246
|
+
groups[fk.tableName] = [];
|
|
247
|
+
}
|
|
248
|
+
groups[fk.tableName].push({
|
|
249
|
+
constraintName: fk.constraintName,
|
|
250
|
+
column: fk.columnName,
|
|
251
|
+
referencedTable: fk.referencedTableName,
|
|
252
|
+
referencedColumn: fk.referencedColumnName,
|
|
253
|
+
updateRule: fk.updateRule,
|
|
254
|
+
deleteRule: fk.deleteRule
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
return groups;
|
|
258
|
+
}
|
|
259
|
+
function analyzeForeignKeyRelationships(foreignKeys) {
|
|
260
|
+
const relationships = [];
|
|
261
|
+
const tableConnections = {};
|
|
262
|
+
// Group by constraint to handle composite foreign keys
|
|
263
|
+
const constraintGroups = {};
|
|
264
|
+
foreignKeys.forEach(fk => {
|
|
265
|
+
if (!constraintGroups[fk.constraintName]) {
|
|
266
|
+
constraintGroups[fk.constraintName] = [];
|
|
267
|
+
}
|
|
268
|
+
constraintGroups[fk.constraintName].push(fk);
|
|
269
|
+
});
|
|
270
|
+
// Analyze each constraint
|
|
271
|
+
Object.keys(constraintGroups).forEach(constraintName => {
|
|
272
|
+
const fks = constraintGroups[constraintName];
|
|
273
|
+
const firstFK = fks[0];
|
|
274
|
+
relationships.push({
|
|
275
|
+
constraintName,
|
|
276
|
+
fromTable: firstFK.tableName,
|
|
277
|
+
toTable: firstFK.referencedTableName,
|
|
278
|
+
columns: fks.map(fk => ({
|
|
279
|
+
from: fk.columnName,
|
|
280
|
+
to: fk.referencedColumnName
|
|
281
|
+
})),
|
|
282
|
+
isComposite: fks.length > 1,
|
|
283
|
+
updateRule: firstFK.updateRule,
|
|
284
|
+
deleteRule: firstFK.deleteRule,
|
|
285
|
+
relationshipStrength: determineRelationshipStrength(firstFK.updateRule, firstFK.deleteRule)
|
|
286
|
+
});
|
|
287
|
+
// Track table connections
|
|
288
|
+
if (!tableConnections[firstFK.tableName]) {
|
|
289
|
+
tableConnections[firstFK.tableName] = new Set();
|
|
290
|
+
}
|
|
291
|
+
tableConnections[firstFK.tableName].add(firstFK.referencedTableName);
|
|
292
|
+
});
|
|
293
|
+
return {
|
|
294
|
+
relationships,
|
|
295
|
+
tableConnections: Object.fromEntries(Object.entries(tableConnections).map(([table, connections]) => [
|
|
296
|
+
table,
|
|
297
|
+
Array.from(connections)
|
|
298
|
+
])),
|
|
299
|
+
totalRelationships: relationships.length,
|
|
300
|
+
compositeRelationships: relationships.filter(r => r.isComposite).length
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function determineRelationshipType(updateRule, deleteRule) {
|
|
304
|
+
const ur = updateRule || 'NO ACTION';
|
|
305
|
+
const dr = deleteRule || 'NO ACTION';
|
|
306
|
+
if (ur === 'CASCADE' || dr === 'CASCADE') {
|
|
307
|
+
return 'strong_dependency'; // Child cannot exist without parent
|
|
308
|
+
}
|
|
309
|
+
else if (dr === 'RESTRICT' || dr === 'NO ACTION') {
|
|
310
|
+
return 'protective'; // Prevents accidental deletion
|
|
311
|
+
}
|
|
312
|
+
else if (dr === 'SET NULL') {
|
|
313
|
+
return 'optional_reference'; // Relationship can be broken
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
return 'unknown';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function determineRelationshipStrength(updateRule, deleteRule) {
|
|
320
|
+
if (deleteRule === 'CASCADE' && updateRule === 'CASCADE') {
|
|
321
|
+
return 'strong';
|
|
322
|
+
}
|
|
323
|
+
else if (deleteRule === 'RESTRICT' || updateRule === 'RESTRICT') {
|
|
324
|
+
return 'medium';
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
return 'weak';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function analyzeIntegrityRules(foreignKeys) {
|
|
331
|
+
const rules = {
|
|
332
|
+
cascade: {
|
|
333
|
+
delete: foreignKeys.filter(fk => fk.deleteRule === 'CASCADE'),
|
|
334
|
+
update: foreignKeys.filter(fk => fk.updateRule === 'CASCADE')
|
|
335
|
+
},
|
|
336
|
+
restrict: {
|
|
337
|
+
delete: foreignKeys.filter(fk => fk.deleteRule === 'RESTRICT' || fk.deleteRule === 'NO ACTION'),
|
|
338
|
+
update: foreignKeys.filter(fk => fk.updateRule === 'RESTRICT' || fk.updateRule === 'NO ACTION')
|
|
339
|
+
},
|
|
340
|
+
setNull: {
|
|
341
|
+
delete: foreignKeys.filter(fk => fk.deleteRule === 'SET NULL'),
|
|
342
|
+
update: foreignKeys.filter(fk => fk.updateRule === 'SET NULL')
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
return {
|
|
346
|
+
rules,
|
|
347
|
+
summary: {
|
|
348
|
+
cascadeDeleteTables: [...new Set(rules.cascade.delete.map(fk => fk.tableName))],
|
|
349
|
+
protectedTables: [...new Set(rules.restrict.delete.map(fk => fk.referencedTableName))],
|
|
350
|
+
weaklyReferencedTables: [...new Set(rules.setNull.delete.map(fk => fk.referencedTableName))]
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function identifyPotentialIssues(foreignKeys) {
|
|
355
|
+
const issues = [];
|
|
356
|
+
// Check for circular references
|
|
357
|
+
const tableGraph = {};
|
|
358
|
+
foreignKeys.forEach(fk => {
|
|
359
|
+
if (!tableGraph[fk.tableName]) {
|
|
360
|
+
tableGraph[fk.tableName] = [];
|
|
361
|
+
}
|
|
362
|
+
tableGraph[fk.tableName].push(fk.referencedTableName);
|
|
363
|
+
});
|
|
364
|
+
// Simple cycle detection (this is a basic check, not comprehensive)
|
|
365
|
+
Object.keys(tableGraph).forEach(table => {
|
|
366
|
+
tableGraph[table].forEach(referenced => {
|
|
367
|
+
if (tableGraph[referenced] && tableGraph[referenced].includes(table)) {
|
|
368
|
+
issues.push(`Potential circular reference detected between tables '${table}' and '${referenced}'`);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
// Check for many cascade delete relationships from same parent
|
|
373
|
+
const cascadeParents = {};
|
|
374
|
+
foreignKeys.filter(fk => fk.deleteRule === 'CASCADE').forEach(fk => {
|
|
375
|
+
cascadeParents[fk.referencedTableName] = (cascadeParents[fk.referencedTableName] || 0) + 1;
|
|
376
|
+
});
|
|
377
|
+
Object.entries(cascadeParents).forEach(([table, count]) => {
|
|
378
|
+
if (count > 5) {
|
|
379
|
+
issues.push(`Table '${table}' has ${count} cascade delete relationships - consider the impact of deletions`);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// Check for inconsistent naming patterns
|
|
383
|
+
const constraintNames = foreignKeys.map(fk => fk.constraintName);
|
|
384
|
+
const hasConsistentNaming = constraintNames.every(name => name.startsWith('fk_') || name.startsWith('FK_') ||
|
|
385
|
+
name.includes('_fk') || name.includes('_FK'));
|
|
386
|
+
if (!hasConsistentNaming && constraintNames.length > 1) {
|
|
387
|
+
issues.push('Foreign key constraint names follow inconsistent naming patterns');
|
|
388
|
+
}
|
|
389
|
+
return issues;
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=get-foreign-keys.js.map
|