@connorbritain/mssql-mcp-core 0.1.0
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/dist/audit/AuditLogger.d.ts +37 -0
- package/dist/audit/AuditLogger.d.ts.map +1 -0
- package/dist/audit/AuditLogger.js +145 -0
- package/dist/audit/AuditLogger.js.map +1 -0
- package/dist/config/EnvironmentManager.d.ts +75 -0
- package/dist/config/EnvironmentManager.d.ts.map +1 -0
- package/dist/config/EnvironmentManager.js +305 -0
- package/dist/config/EnvironmentManager.js.map +1 -0
- package/dist/config/ScriptManager.d.ts +69 -0
- package/dist/config/ScriptManager.d.ts.map +1 -0
- package/dist/config/ScriptManager.js +166 -0
- package/dist/config/ScriptManager.js.map +1 -0
- package/dist/config/SecretResolver.d.ts +66 -0
- package/dist/config/SecretResolver.d.ts.map +1 -0
- package/dist/config/SecretResolver.js +230 -0
- package/dist/config/SecretResolver.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/routing/IntentRouter.d.ts +17 -0
- package/dist/routing/IntentRouter.d.ts.map +1 -0
- package/dist/routing/IntentRouter.js +243 -0
- package/dist/routing/IntentRouter.js.map +1 -0
- package/dist/server/createMcpServer.d.ts +7 -0
- package/dist/server/createMcpServer.d.ts.map +1 -0
- package/dist/server/createMcpServer.js +100 -0
- package/dist/server/createMcpServer.js.map +1 -0
- package/dist/server/toolsets.d.ts +42 -0
- package/dist/server/toolsets.d.ts.map +1 -0
- package/dist/server/toolsets.js +303 -0
- package/dist/server/toolsets.js.map +1 -0
- package/dist/server/wrapToolRun.d.ts +13 -0
- package/dist/server/wrapToolRun.d.ts.map +1 -0
- package/dist/server/wrapToolRun.js +102 -0
- package/dist/server/wrapToolRun.js.map +1 -0
- package/dist/shims.d.ts +2 -0
- package/dist/shims.d.ts.map +1 -0
- package/dist/shims.js +15 -0
- package/dist/shims.js.map +1 -0
- package/dist/tools/CreateIndexTool.d.ts +24 -0
- package/dist/tools/CreateIndexTool.d.ts.map +1 -0
- package/dist/tools/CreateIndexTool.js +64 -0
- package/dist/tools/CreateIndexTool.js.map +1 -0
- package/dist/tools/CreateTableTool.d.ts +12 -0
- package/dist/tools/CreateTableTool.d.ts.map +1 -0
- package/dist/tools/CreateTableTool.js +49 -0
- package/dist/tools/CreateTableTool.js.map +1 -0
- package/dist/tools/DeleteDataTool.d.ts +56 -0
- package/dist/tools/DeleteDataTool.d.ts.map +1 -0
- package/dist/tools/DeleteDataTool.js +103 -0
- package/dist/tools/DeleteDataTool.js.map +1 -0
- package/dist/tools/DescribeTableTool.d.ts +32 -0
- package/dist/tools/DescribeTableTool.d.ts.map +1 -0
- package/dist/tools/DescribeTableTool.js +116 -0
- package/dist/tools/DescribeTableTool.js.map +1 -0
- package/dist/tools/DropTableTool.d.ts +12 -0
- package/dist/tools/DropTableTool.d.ts.map +1 -0
- package/dist/tools/DropTableTool.js +37 -0
- package/dist/tools/DropTableTool.js.map +1 -0
- package/dist/tools/ExplainQueryTool.d.ts +24 -0
- package/dist/tools/ExplainQueryTool.d.ts.map +1 -0
- package/dist/tools/ExplainQueryTool.js +98 -0
- package/dist/tools/ExplainQueryTool.js.map +1 -0
- package/dist/tools/InsertDataTool.d.ts +17 -0
- package/dist/tools/InsertDataTool.d.ts.map +1 -0
- package/dist/tools/InsertDataTool.js +102 -0
- package/dist/tools/InsertDataTool.js.map +1 -0
- package/dist/tools/InspectDependenciesTool.d.ts +46 -0
- package/dist/tools/InspectDependenciesTool.d.ts.map +1 -0
- package/dist/tools/InspectDependenciesTool.js +215 -0
- package/dist/tools/InspectDependenciesTool.js.map +1 -0
- package/dist/tools/ListDatabasesTool.d.ts +27 -0
- package/dist/tools/ListDatabasesTool.d.ts.map +1 -0
- package/dist/tools/ListDatabasesTool.js +107 -0
- package/dist/tools/ListDatabasesTool.js.map +1 -0
- package/dist/tools/ListEnvironmentsTool.d.ts +49 -0
- package/dist/tools/ListEnvironmentsTool.d.ts.map +1 -0
- package/dist/tools/ListEnvironmentsTool.js +73 -0
- package/dist/tools/ListEnvironmentsTool.js.map +1 -0
- package/dist/tools/ListScriptsTool.d.ts +41 -0
- package/dist/tools/ListScriptsTool.d.ts.map +1 -0
- package/dist/tools/ListScriptsTool.js +86 -0
- package/dist/tools/ListScriptsTool.js.map +1 -0
- package/dist/tools/ListTableTool.d.ts +24 -0
- package/dist/tools/ListTableTool.d.ts.map +1 -0
- package/dist/tools/ListTableTool.js +85 -0
- package/dist/tools/ListTableTool.js.map +1 -0
- package/dist/tools/ProfileTableTool.d.ts +78 -0
- package/dist/tools/ProfileTableTool.d.ts.map +1 -0
- package/dist/tools/ProfileTableTool.js +373 -0
- package/dist/tools/ProfileTableTool.js.map +1 -0
- package/dist/tools/ReadDataTool.d.ts +61 -0
- package/dist/tools/ReadDataTool.d.ts.map +1 -0
- package/dist/tools/ReadDataTool.js +299 -0
- package/dist/tools/ReadDataTool.js.map +1 -0
- package/dist/tools/RelationshipInspectorTool.d.ts +46 -0
- package/dist/tools/RelationshipInspectorTool.d.ts.map +1 -0
- package/dist/tools/RelationshipInspectorTool.js +156 -0
- package/dist/tools/RelationshipInspectorTool.js.map +1 -0
- package/dist/tools/RunScriptTool.d.ts +214 -0
- package/dist/tools/RunScriptTool.d.ts.map +1 -0
- package/dist/tools/RunScriptTool.js +186 -0
- package/dist/tools/RunScriptTool.js.map +1 -0
- package/dist/tools/SearchSchemaTool.d.ts +88 -0
- package/dist/tools/SearchSchemaTool.d.ts.map +1 -0
- package/dist/tools/SearchSchemaTool.js +237 -0
- package/dist/tools/SearchSchemaTool.js.map +1 -0
- package/dist/tools/TestConnectionTool.d.ts +38 -0
- package/dist/tools/TestConnectionTool.d.ts.map +1 -0
- package/dist/tools/TestConnectionTool.js +156 -0
- package/dist/tools/TestConnectionTool.js.map +1 -0
- package/dist/tools/UpdateDataTool.d.ts +61 -0
- package/dist/tools/UpdateDataTool.d.ts.map +1 -0
- package/dist/tools/UpdateDataTool.js +117 -0
- package/dist/tools/UpdateDataTool.js.map +1 -0
- package/dist/tools/ValidateEnvironmentConfigTool.d.ts +51 -0
- package/dist/tools/ValidateEnvironmentConfigTool.d.ts.map +1 -0
- package/dist/tools/ValidateEnvironmentConfigTool.js +320 -0
- package/dist/tools/ValidateEnvironmentConfigTool.js.map +1 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +22 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getScriptManager } from "../config/ScriptManager.js";
|
|
2
|
+
export class ListScriptsTool {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.name = "list_scripts";
|
|
5
|
+
this.description = "Lists available named SQL scripts. Scripts are pre-approved SQL templates that can be executed with parameters.";
|
|
6
|
+
this.inputSchema = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
environment: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "Filter scripts to those allowed in this environment (optional)",
|
|
12
|
+
},
|
|
13
|
+
tier: {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: ["reader", "writer", "admin"],
|
|
16
|
+
description: "Filter scripts by minimum tier requirement (optional)",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: [],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async run(params) {
|
|
23
|
+
const scriptManager = getScriptManager();
|
|
24
|
+
if (!scriptManager.isConfigured()) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
message: "Named scripts are not configured. Set scriptsPath in environments.json or SCRIPTS_PATH env var.",
|
|
28
|
+
scriptsPath: null,
|
|
29
|
+
scripts: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const allScripts = scriptManager.listScripts();
|
|
33
|
+
if (allScripts.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
message: "No scripts found in scripts directory.",
|
|
37
|
+
scriptsPath: scriptManager.getScriptsPath(),
|
|
38
|
+
scripts: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Filter by environment if specified
|
|
42
|
+
let filteredScripts = allScripts;
|
|
43
|
+
if (params?.environment) {
|
|
44
|
+
filteredScripts = filteredScripts.filter((script) => {
|
|
45
|
+
const check = scriptManager.canRunInEnvironment(script.name, params.environment);
|
|
46
|
+
return check.allowed;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Filter by tier if specified
|
|
50
|
+
if (params?.tier) {
|
|
51
|
+
const tierOrder = ["reader", "writer", "admin"];
|
|
52
|
+
const requestedTierIndex = tierOrder.indexOf(params.tier);
|
|
53
|
+
filteredScripts = filteredScripts.filter((script) => {
|
|
54
|
+
if (!script.tier)
|
|
55
|
+
return true; // No tier restriction
|
|
56
|
+
const scriptTierIndex = tierOrder.indexOf(script.tier);
|
|
57
|
+
return scriptTierIndex <= requestedTierIndex;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Map to output format (exclude SQL content for listing)
|
|
61
|
+
const scriptsOutput = filteredScripts.map((script) => ({
|
|
62
|
+
name: script.name,
|
|
63
|
+
description: script.description,
|
|
64
|
+
parameters: script.parameters?.map((p) => ({
|
|
65
|
+
name: p.name,
|
|
66
|
+
type: p.type,
|
|
67
|
+
required: p.required ?? false,
|
|
68
|
+
default: p.default,
|
|
69
|
+
description: p.description,
|
|
70
|
+
})),
|
|
71
|
+
tier: script.tier,
|
|
72
|
+
requiresApproval: script.requiresApproval,
|
|
73
|
+
readonly: script.readonly,
|
|
74
|
+
allowedEnvironments: script.allowedEnvironments,
|
|
75
|
+
deniedEnvironments: script.deniedEnvironments,
|
|
76
|
+
}));
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
scriptsPath: scriptManager.getScriptsPath(),
|
|
80
|
+
totalScripts: allScripts.length,
|
|
81
|
+
filteredCount: scriptsOutput.length,
|
|
82
|
+
scripts: scriptsOutput,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=ListScriptsTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListScriptsTool.js","sourceRoot":"","sources":["../../src/tools/ListScriptsTool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import sql from "mssql";
|
|
2
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare class ListTableTool implements Tool {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: any;
|
|
8
|
+
run(params: any): Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
error: string;
|
|
12
|
+
database?: undefined;
|
|
13
|
+
tableCount?: undefined;
|
|
14
|
+
tables?: undefined;
|
|
15
|
+
} | {
|
|
16
|
+
success: boolean;
|
|
17
|
+
message: string;
|
|
18
|
+
database: any;
|
|
19
|
+
tableCount: number;
|
|
20
|
+
tables: sql.IRecordSet<any>;
|
|
21
|
+
error?: undefined;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ListTableTool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListTableTool.d.ts","sourceRoot":"","sources":["../../src/tools/ListTableTool.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,OAAO,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAG1D,qBAAa,aAAc,YAAW,IAAI;IACxC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;IACnB,IAAI,SAAiB;IACrB,WAAW,SAE+E;IAC1F,WAAW,EAiBN,GAAG,CAAC;IAEH,GAAG,CAAC,MAAM,EAAE,GAAG;;;;;;;;;;;;;;;CA4DtB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sql from "mssql";
|
|
2
|
+
import { getEnvironmentManager } from "../config/EnvironmentManager.js";
|
|
3
|
+
export class ListTableTool {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.name = "list_tables";
|
|
6
|
+
this.description = "Lists tables in an MSSQL Database, optionally filtered by schema. " +
|
|
7
|
+
"For server-level access environments, you can specify a database to list tables from.";
|
|
8
|
+
this.inputSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
database: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Optional: Target database name for server-level access environments.",
|
|
14
|
+
},
|
|
15
|
+
schemas: {
|
|
16
|
+
type: "array",
|
|
17
|
+
description: "Schemas to filter by (optional)",
|
|
18
|
+
items: {
|
|
19
|
+
type: "string",
|
|
20
|
+
},
|
|
21
|
+
minItems: 0,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async run(params) {
|
|
28
|
+
try {
|
|
29
|
+
const { database, schemas, environment } = params ?? {};
|
|
30
|
+
// Validate database access if specified
|
|
31
|
+
if (database) {
|
|
32
|
+
const envManager = getEnvironmentManager();
|
|
33
|
+
const dbCheck = envManager.isDatabaseAllowed(environment, database);
|
|
34
|
+
if (!dbCheck.allowed) {
|
|
35
|
+
return {
|
|
36
|
+
success: false,
|
|
37
|
+
message: dbCheck.reason || `Access to database '${database}' is not allowed.`,
|
|
38
|
+
error: "DATABASE_ACCESS_DENIED",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const request = new sql.Request(params.pool);
|
|
43
|
+
const schemaFilter = schemas && schemas.length > 0
|
|
44
|
+
? `AND TABLE_SCHEMA IN (${schemas.map((p) => `'${p.replace(/'/g, "''")}'`).join(", ")})`
|
|
45
|
+
: "";
|
|
46
|
+
// Build query with optional database prefix
|
|
47
|
+
let query;
|
|
48
|
+
if (database) {
|
|
49
|
+
const safeDbName = database.replace(/]/g, "]]");
|
|
50
|
+
query = `
|
|
51
|
+
USE [${safeDbName}];
|
|
52
|
+
SELECT TABLE_SCHEMA + '.' + TABLE_NAME AS table_name, TABLE_SCHEMA, TABLE_NAME
|
|
53
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
54
|
+
WHERE TABLE_TYPE = 'BASE TABLE' ${schemaFilter}
|
|
55
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
query = `
|
|
60
|
+
SELECT TABLE_SCHEMA + '.' + TABLE_NAME AS table_name, TABLE_SCHEMA, TABLE_NAME
|
|
61
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
62
|
+
WHERE TABLE_TYPE = 'BASE TABLE' ${schemaFilter}
|
|
63
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
const result = await request.query(query);
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
message: `Found ${result.recordset.length} table(s)${database ? ` in [${database}]` : ""}`,
|
|
70
|
+
database: database || undefined,
|
|
71
|
+
tableCount: result.recordset.length,
|
|
72
|
+
tables: result.recordset,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error("Error listing tables:", error);
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
message: `Failed to list tables: ${error}`,
|
|
80
|
+
error: "QUERY_FAILED",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=ListTableTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListTableTool.js","sourceRoot":"","sources":["../../src/tools/ListTableTool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
type ProfileParams = {
|
|
3
|
+
tableName: string;
|
|
4
|
+
schemaName?: string;
|
|
5
|
+
sampleSize?: number;
|
|
6
|
+
includeDistributions?: boolean;
|
|
7
|
+
topValuesLimit?: number;
|
|
8
|
+
columnsToProfile?: string[];
|
|
9
|
+
includeSamples?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type NumericStats = {
|
|
12
|
+
min: number;
|
|
13
|
+
max: number;
|
|
14
|
+
avg: number;
|
|
15
|
+
median?: number;
|
|
16
|
+
p90?: number;
|
|
17
|
+
};
|
|
18
|
+
type StringStats = {
|
|
19
|
+
minLength: number;
|
|
20
|
+
maxLength: number;
|
|
21
|
+
avgLength: number;
|
|
22
|
+
emptyCount: number;
|
|
23
|
+
};
|
|
24
|
+
type DateStats = {
|
|
25
|
+
earliest: string;
|
|
26
|
+
latest: string;
|
|
27
|
+
range: string;
|
|
28
|
+
};
|
|
29
|
+
type TopValue = {
|
|
30
|
+
value: any;
|
|
31
|
+
count: number;
|
|
32
|
+
percentage: number;
|
|
33
|
+
};
|
|
34
|
+
type ColumnProfile = {
|
|
35
|
+
columnName: string;
|
|
36
|
+
dataType: string;
|
|
37
|
+
isNullable: boolean;
|
|
38
|
+
nullCount: number;
|
|
39
|
+
nullPercentage: number;
|
|
40
|
+
distinctCount: number;
|
|
41
|
+
cardinality: "unique" | "high" | "medium" | "low";
|
|
42
|
+
numericStats?: NumericStats;
|
|
43
|
+
stringStats?: StringStats;
|
|
44
|
+
dateStats?: DateStats;
|
|
45
|
+
topValues?: TopValue[];
|
|
46
|
+
};
|
|
47
|
+
type ProfileResult = {
|
|
48
|
+
success: boolean;
|
|
49
|
+
message?: string;
|
|
50
|
+
tableName?: string;
|
|
51
|
+
schemaName?: string;
|
|
52
|
+
rowCount?: number;
|
|
53
|
+
columnCount?: number;
|
|
54
|
+
sampleSize?: number;
|
|
55
|
+
columns?: ColumnProfile[];
|
|
56
|
+
samples?: Record<string, unknown>[];
|
|
57
|
+
};
|
|
58
|
+
export declare class ProfileTableTool implements Tool {
|
|
59
|
+
[key: string]: any;
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
inputSchema: any;
|
|
63
|
+
private static readonly SKIP_TYPES;
|
|
64
|
+
private static readonly NUMERIC_TYPES;
|
|
65
|
+
private static readonly STRING_TYPES;
|
|
66
|
+
private static readonly DATE_TYPES;
|
|
67
|
+
private normalizeLimit;
|
|
68
|
+
private classifyCardinality;
|
|
69
|
+
private formatDateRange;
|
|
70
|
+
private escapeIdentifier;
|
|
71
|
+
private isNumericType;
|
|
72
|
+
private isStringType;
|
|
73
|
+
private isDateType;
|
|
74
|
+
private shouldSkipType;
|
|
75
|
+
run(params: ProfileParams): Promise<ProfileResult>;
|
|
76
|
+
}
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=ProfileTableTool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProfileTableTool.d.ts","sourceRoot":"","sources":["../../src/tools/ProfileTableTool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import sql from "mssql";
|
|
2
|
+
const clampEnvInt = (value, fallback, min, max) => {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return fallback;
|
|
5
|
+
}
|
|
6
|
+
const parsed = parseInt(value, 10);
|
|
7
|
+
if (Number.isNaN(parsed)) {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
return Math.min(Math.max(parsed, min), max);
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_SAMPLE_SIZE = clampEnvInt(process.env.PROFILE_SAMPLE_SIZE_DEFAULT, 50, 1, 1000);
|
|
13
|
+
const SAMPLE_RETURN_LIMIT = clampEnvInt(process.env.PROFILE_SAMPLE_RETURN_LIMIT, 10, 1, 100);
|
|
14
|
+
export class ProfileTableTool {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.name = "profile_table";
|
|
17
|
+
this.description = "Profiles a table by analyzing column statistics, data distributions, and sample records. Returns metadata, cardinality info, null counts, and representative samples for each column.";
|
|
18
|
+
this.inputSchema = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
tableName: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Name of table to profile (schema.table or table)",
|
|
24
|
+
},
|
|
25
|
+
schemaName: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Explicit schema (defaults to 'dbo')",
|
|
28
|
+
},
|
|
29
|
+
sampleSize: {
|
|
30
|
+
type: "number",
|
|
31
|
+
description: "Number of sample rows (default 100, max 1000)",
|
|
32
|
+
},
|
|
33
|
+
includeSamples: {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description: "Return sampled rows used for profiling (default false)",
|
|
36
|
+
},
|
|
37
|
+
includeDistributions: {
|
|
38
|
+
type: "boolean",
|
|
39
|
+
description: "Include top value frequencies (default true)",
|
|
40
|
+
},
|
|
41
|
+
topValuesLimit: {
|
|
42
|
+
type: "number",
|
|
43
|
+
description: "Max distinct values per column (default 10, max 50)",
|
|
44
|
+
},
|
|
45
|
+
columnsToProfile: {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: { type: "string" },
|
|
48
|
+
description: "Specific columns to profile (default: all)",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ["tableName"],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
normalizeLimit(value, defaultVal, max) {
|
|
55
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
|
|
56
|
+
return defaultVal;
|
|
57
|
+
}
|
|
58
|
+
return Math.min(Math.floor(value), max);
|
|
59
|
+
}
|
|
60
|
+
classifyCardinality(distinctCount, rowCount) {
|
|
61
|
+
if (rowCount === 0)
|
|
62
|
+
return "low";
|
|
63
|
+
const ratio = distinctCount / rowCount;
|
|
64
|
+
if (ratio > 0.95)
|
|
65
|
+
return "unique";
|
|
66
|
+
if (ratio > 0.5)
|
|
67
|
+
return "high";
|
|
68
|
+
if (ratio > 0.1)
|
|
69
|
+
return "medium";
|
|
70
|
+
return "low";
|
|
71
|
+
}
|
|
72
|
+
formatDateRange(earliest, latest) {
|
|
73
|
+
const diffMs = latest.getTime() - earliest.getTime();
|
|
74
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
75
|
+
if (diffDays < 1)
|
|
76
|
+
return "less than 1 day";
|
|
77
|
+
if (diffDays < 30)
|
|
78
|
+
return `${diffDays} day${diffDays > 1 ? "s" : ""}`;
|
|
79
|
+
const diffMonths = Math.floor(diffDays / 30);
|
|
80
|
+
if (diffMonths < 12)
|
|
81
|
+
return `${diffMonths} month${diffMonths > 1 ? "s" : ""}`;
|
|
82
|
+
const years = Math.floor(diffMonths / 12);
|
|
83
|
+
const remainingMonths = diffMonths % 12;
|
|
84
|
+
if (remainingMonths === 0)
|
|
85
|
+
return `${years} year${years > 1 ? "s" : ""}`;
|
|
86
|
+
return `${years} year${years > 1 ? "s" : ""}, ${remainingMonths} month${remainingMonths > 1 ? "s" : ""}`;
|
|
87
|
+
}
|
|
88
|
+
escapeIdentifier(name) {
|
|
89
|
+
return `[${name.replace(/\]/g, "]]")}]`;
|
|
90
|
+
}
|
|
91
|
+
isNumericType(dataType) {
|
|
92
|
+
return ProfileTableTool.NUMERIC_TYPES.includes(dataType.toLowerCase());
|
|
93
|
+
}
|
|
94
|
+
isStringType(dataType) {
|
|
95
|
+
return ProfileTableTool.STRING_TYPES.includes(dataType.toLowerCase());
|
|
96
|
+
}
|
|
97
|
+
isDateType(dataType) {
|
|
98
|
+
return ProfileTableTool.DATE_TYPES.includes(dataType.toLowerCase());
|
|
99
|
+
}
|
|
100
|
+
shouldSkipType(dataType) {
|
|
101
|
+
return ProfileTableTool.SKIP_TYPES.includes(dataType.toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
async run(params) {
|
|
104
|
+
try {
|
|
105
|
+
const pool = params.pool;
|
|
106
|
+
const tableName = params.tableName?.trim();
|
|
107
|
+
if (!tableName) {
|
|
108
|
+
return { success: false, message: "tableName is required." };
|
|
109
|
+
}
|
|
110
|
+
const schemaName = params.schemaName?.trim() || "dbo";
|
|
111
|
+
const sampleSize = this.normalizeLimit(params.sampleSize, DEFAULT_SAMPLE_SIZE, 1000);
|
|
112
|
+
const includeDistributions = params.includeDistributions !== false;
|
|
113
|
+
const includeSamples = params.includeSamples === true;
|
|
114
|
+
const topValuesLimit = this.normalizeLimit(params.topValuesLimit, 10, 50);
|
|
115
|
+
const columnsToProfile = params.columnsToProfile?.map((c) => c.trim()).filter(Boolean);
|
|
116
|
+
// 1. Validate table exists and get columns
|
|
117
|
+
const metaRequest = new sql.Request(pool);
|
|
118
|
+
metaRequest.input("schemaName", sql.NVarChar, schemaName);
|
|
119
|
+
metaRequest.input("tableName", sql.NVarChar, tableName);
|
|
120
|
+
const metaResult = await metaRequest.query(`
|
|
121
|
+
SELECT
|
|
122
|
+
c.COLUMN_NAME AS columnName,
|
|
123
|
+
c.DATA_TYPE AS dataType,
|
|
124
|
+
CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END AS isNullable
|
|
125
|
+
FROM INFORMATION_SCHEMA.COLUMNS c
|
|
126
|
+
INNER JOIN INFORMATION_SCHEMA.TABLES t
|
|
127
|
+
ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME
|
|
128
|
+
WHERE c.TABLE_SCHEMA = @schemaName AND c.TABLE_NAME = @tableName
|
|
129
|
+
ORDER BY c.ORDINAL_POSITION
|
|
130
|
+
`);
|
|
131
|
+
if (!metaResult.recordset.length) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
message: `Table [${schemaName}].[${tableName}] not found or has no columns.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// Filter columns if requested
|
|
138
|
+
let columns = [...metaResult.recordset];
|
|
139
|
+
if (columnsToProfile && columnsToProfile.length) {
|
|
140
|
+
const requested = new Set(columnsToProfile.map((c) => c.toLowerCase()));
|
|
141
|
+
columns = columns.filter((c) => requested.has(c.columnName.toLowerCase()));
|
|
142
|
+
if (!columns.length) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
message: `None of the requested columns exist in [${schemaName}].[${tableName}].`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Filter out binary/blob types
|
|
150
|
+
columns = columns.filter((c) => !this.shouldSkipType(c.dataType));
|
|
151
|
+
// 2. Get total row count
|
|
152
|
+
const countRequest = new sql.Request(pool);
|
|
153
|
+
const fqTable = `${this.escapeIdentifier(schemaName)}.${this.escapeIdentifier(tableName)}`;
|
|
154
|
+
const countResult = await countRequest.query(`SELECT COUNT(*) AS cnt FROM ${fqTable}`);
|
|
155
|
+
const rowCount = countResult.recordset[0]?.cnt ?? 0;
|
|
156
|
+
let sampleRows;
|
|
157
|
+
if (rowCount === 0) {
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
tableName,
|
|
161
|
+
schemaName,
|
|
162
|
+
rowCount: 0,
|
|
163
|
+
columnCount: columns.length,
|
|
164
|
+
sampleSize: 0,
|
|
165
|
+
columns: columns.map((c) => ({
|
|
166
|
+
columnName: c.columnName,
|
|
167
|
+
dataType: c.dataType,
|
|
168
|
+
isNullable: Boolean(c.isNullable),
|
|
169
|
+
nullCount: 0,
|
|
170
|
+
nullPercentage: 0,
|
|
171
|
+
distinctCount: 0,
|
|
172
|
+
cardinality: "low",
|
|
173
|
+
})),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (includeSamples) {
|
|
177
|
+
const sampleRequest = new sql.Request(pool);
|
|
178
|
+
const sampleQuery = `
|
|
179
|
+
SELECT TOP (${sampleSize}) *
|
|
180
|
+
FROM ${fqTable}
|
|
181
|
+
ORDER BY NEWID()
|
|
182
|
+
`;
|
|
183
|
+
const sampleResult = await sampleRequest.query(sampleQuery);
|
|
184
|
+
sampleRows = (sampleResult.recordset ?? []).slice(0, SAMPLE_RETURN_LIMIT);
|
|
185
|
+
}
|
|
186
|
+
// 3. Profile each column
|
|
187
|
+
const columnProfiles = [];
|
|
188
|
+
for (const col of columns) {
|
|
189
|
+
const colName = this.escapeIdentifier(col.columnName);
|
|
190
|
+
const profile = {
|
|
191
|
+
columnName: col.columnName,
|
|
192
|
+
dataType: col.dataType,
|
|
193
|
+
isNullable: Boolean(col.isNullable),
|
|
194
|
+
nullCount: 0,
|
|
195
|
+
nullPercentage: 0,
|
|
196
|
+
distinctCount: 0,
|
|
197
|
+
cardinality: "low",
|
|
198
|
+
};
|
|
199
|
+
// Base stats: null count and distinct count
|
|
200
|
+
const baseRequest = new sql.Request(pool);
|
|
201
|
+
const baseResult = await baseRequest.query(`
|
|
202
|
+
SELECT
|
|
203
|
+
SUM(CASE WHEN ${colName} IS NULL THEN 1 ELSE 0 END) AS nullCount,
|
|
204
|
+
COUNT(DISTINCT ${colName}) AS distinctCount
|
|
205
|
+
FROM ${fqTable}
|
|
206
|
+
`);
|
|
207
|
+
const baseRow = baseResult.recordset[0];
|
|
208
|
+
profile.nullCount = baseRow?.nullCount ?? 0;
|
|
209
|
+
profile.nullPercentage = rowCount > 0 ? Number(((profile.nullCount / rowCount) * 100).toFixed(2)) : 0;
|
|
210
|
+
profile.distinctCount = baseRow?.distinctCount ?? 0;
|
|
211
|
+
profile.cardinality = this.classifyCardinality(profile.distinctCount, rowCount);
|
|
212
|
+
// Type-specific stats
|
|
213
|
+
if (this.isNumericType(col.dataType)) {
|
|
214
|
+
const numRequest = new sql.Request(pool);
|
|
215
|
+
const numResult = await numRequest.query(`
|
|
216
|
+
SELECT
|
|
217
|
+
MIN(${colName}) AS minVal,
|
|
218
|
+
MAX(${colName}) AS maxVal,
|
|
219
|
+
AVG(CAST(${colName} AS FLOAT)) AS avgVal
|
|
220
|
+
FROM ${fqTable}
|
|
221
|
+
WHERE ${colName} IS NOT NULL
|
|
222
|
+
`);
|
|
223
|
+
const numRow = numResult.recordset[0];
|
|
224
|
+
if (numRow && numRow.minVal !== null) {
|
|
225
|
+
profile.numericStats = {
|
|
226
|
+
min: numRow.minVal,
|
|
227
|
+
max: numRow.maxVal,
|
|
228
|
+
avg: Number(Number(numRow.avgVal).toFixed(4)),
|
|
229
|
+
};
|
|
230
|
+
const percentileRequest = new sql.Request(pool);
|
|
231
|
+
const percentileResult = await percentileRequest.query(`
|
|
232
|
+
SELECT TOP 1
|
|
233
|
+
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ${colName}) OVER () AS medianVal,
|
|
234
|
+
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY ${colName}) OVER () AS p90Val
|
|
235
|
+
FROM ${fqTable}
|
|
236
|
+
WHERE ${colName} IS NOT NULL
|
|
237
|
+
`);
|
|
238
|
+
const percentileRow = percentileResult.recordset[0];
|
|
239
|
+
if (percentileRow) {
|
|
240
|
+
if (percentileRow.medianVal !== null && percentileRow.medianVal !== undefined) {
|
|
241
|
+
profile.numericStats.median = Number(Number(percentileRow.medianVal).toFixed(4));
|
|
242
|
+
}
|
|
243
|
+
if (percentileRow.p90Val !== null && percentileRow.p90Val !== undefined) {
|
|
244
|
+
profile.numericStats.p90 = Number(Number(percentileRow.p90Val).toFixed(4));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (this.isStringType(col.dataType)) {
|
|
250
|
+
const strRequest = new sql.Request(pool);
|
|
251
|
+
const strResult = await strRequest.query(`
|
|
252
|
+
SELECT
|
|
253
|
+
MIN(LEN(${colName})) AS minLength,
|
|
254
|
+
MAX(LEN(${colName})) AS maxLength,
|
|
255
|
+
AVG(CAST(LEN(${colName}) AS FLOAT)) AS avgLength,
|
|
256
|
+
SUM(CASE WHEN ${colName} = '' THEN 1 ELSE 0 END) AS emptyCount
|
|
257
|
+
FROM ${fqTable}
|
|
258
|
+
WHERE ${colName} IS NOT NULL
|
|
259
|
+
`);
|
|
260
|
+
const strRow = strResult.recordset[0];
|
|
261
|
+
if (strRow && strRow.minLength !== null) {
|
|
262
|
+
profile.stringStats = {
|
|
263
|
+
minLength: strRow.minLength,
|
|
264
|
+
maxLength: strRow.maxLength,
|
|
265
|
+
avgLength: Number(Number(strRow.avgLength).toFixed(2)),
|
|
266
|
+
emptyCount: strRow.emptyCount ?? 0,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (this.isDateType(col.dataType)) {
|
|
271
|
+
const dateRequest = new sql.Request(pool);
|
|
272
|
+
const dateResult = await dateRequest.query(`
|
|
273
|
+
SELECT
|
|
274
|
+
MIN(${colName}) AS earliest,
|
|
275
|
+
MAX(${colName}) AS latest
|
|
276
|
+
FROM ${fqTable}
|
|
277
|
+
WHERE ${colName} IS NOT NULL
|
|
278
|
+
`);
|
|
279
|
+
const dateRow = dateResult.recordset[0];
|
|
280
|
+
if (dateRow && dateRow.earliest !== null) {
|
|
281
|
+
const earliest = new Date(dateRow.earliest);
|
|
282
|
+
const latest = new Date(dateRow.latest);
|
|
283
|
+
profile.dateStats = {
|
|
284
|
+
earliest: earliest.toISOString(),
|
|
285
|
+
latest: latest.toISOString(),
|
|
286
|
+
range: this.formatDateRange(earliest, latest),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Top values distribution
|
|
291
|
+
if (includeDistributions && profile.distinctCount > 0 && profile.distinctCount <= rowCount) {
|
|
292
|
+
const topRequest = new sql.Request(pool);
|
|
293
|
+
topRequest.input("topLimit", sql.Int, topValuesLimit);
|
|
294
|
+
const topResult = await topRequest.query(`
|
|
295
|
+
SELECT TOP (@topLimit)
|
|
296
|
+
${colName} AS value,
|
|
297
|
+
COUNT(*) AS cnt
|
|
298
|
+
FROM ${fqTable}
|
|
299
|
+
WHERE ${colName} IS NOT NULL
|
|
300
|
+
GROUP BY ${colName}
|
|
301
|
+
ORDER BY cnt DESC
|
|
302
|
+
`);
|
|
303
|
+
if (topResult.recordset.length) {
|
|
304
|
+
profile.topValues = topResult.recordset.map((r) => ({
|
|
305
|
+
value: r.value,
|
|
306
|
+
count: r.cnt,
|
|
307
|
+
percentage: Number(((r.cnt / rowCount) * 100).toFixed(2)),
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
columnProfiles.push(profile);
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
success: true,
|
|
315
|
+
tableName,
|
|
316
|
+
schemaName,
|
|
317
|
+
rowCount,
|
|
318
|
+
columnCount: columnProfiles.length,
|
|
319
|
+
sampleSize: includeSamples ? sampleRows?.length ?? 0 : sampleSize,
|
|
320
|
+
columns: columnProfiles,
|
|
321
|
+
samples: includeSamples ? sampleRows : undefined,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return {
|
|
326
|
+
success: false,
|
|
327
|
+
message: `Failed to profile table: ${error}`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Binary/blob types to skip
|
|
333
|
+
ProfileTableTool.SKIP_TYPES = [
|
|
334
|
+
"image",
|
|
335
|
+
"varbinary",
|
|
336
|
+
"binary",
|
|
337
|
+
"timestamp",
|
|
338
|
+
"rowversion",
|
|
339
|
+
"sql_variant",
|
|
340
|
+
"xml",
|
|
341
|
+
"geography",
|
|
342
|
+
"geometry",
|
|
343
|
+
"hierarchyid",
|
|
344
|
+
];
|
|
345
|
+
ProfileTableTool.NUMERIC_TYPES = [
|
|
346
|
+
"int",
|
|
347
|
+
"bigint",
|
|
348
|
+
"smallint",
|
|
349
|
+
"tinyint",
|
|
350
|
+
"decimal",
|
|
351
|
+
"numeric",
|
|
352
|
+
"float",
|
|
353
|
+
"real",
|
|
354
|
+
"money",
|
|
355
|
+
"smallmoney",
|
|
356
|
+
];
|
|
357
|
+
ProfileTableTool.STRING_TYPES = [
|
|
358
|
+
"char",
|
|
359
|
+
"varchar",
|
|
360
|
+
"nchar",
|
|
361
|
+
"nvarchar",
|
|
362
|
+
"text",
|
|
363
|
+
"ntext",
|
|
364
|
+
];
|
|
365
|
+
ProfileTableTool.DATE_TYPES = [
|
|
366
|
+
"date",
|
|
367
|
+
"datetime",
|
|
368
|
+
"datetime2",
|
|
369
|
+
"smalldatetime",
|
|
370
|
+
"datetimeoffset",
|
|
371
|
+
"time",
|
|
372
|
+
];
|
|
373
|
+
//# sourceMappingURL=ProfileTableTool.js.map
|