@neverinfamous/mysql-mcp 2.2.0 → 2.3.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/.github/workflows/docker-publish.yml +1 -2
- package/CHANGELOG.md +85 -0
- package/CODE_MODE.md +245 -0
- package/DOCKER_README.md +59 -36
- package/README.md +65 -42
- package/VERSION +1 -1
- package/dist/adapters/mysql/MySQLAdapter.d.ts +4 -0
- package/dist/adapters/mysql/MySQLAdapter.d.ts.map +1 -1
- package/dist/adapters/mysql/MySQLAdapter.js +9 -0
- package/dist/adapters/mysql/MySQLAdapter.js.map +1 -1
- package/dist/adapters/mysql/prompts/index.d.ts +8 -1
- package/dist/adapters/mysql/prompts/index.d.ts.map +1 -1
- package/dist/adapters/mysql/prompts/index.js +8 -1
- package/dist/adapters/mysql/prompts/index.js.map +1 -1
- package/dist/adapters/mysql/prompts/routerSetup.d.ts.map +1 -1
- package/dist/adapters/mysql/prompts/routerSetup.js +5 -0
- package/dist/adapters/mysql/prompts/routerSetup.js.map +1 -1
- package/dist/adapters/mysql/resources/capabilities.d.ts.map +1 -1
- package/dist/adapters/mysql/resources/capabilities.js +6 -5
- package/dist/adapters/mysql/resources/capabilities.js.map +1 -1
- package/dist/adapters/mysql/resources/index.d.ts +9 -1
- package/dist/adapters/mysql/resources/index.d.ts.map +1 -1
- package/dist/adapters/mysql/resources/index.js +9 -1
- package/dist/adapters/mysql/resources/index.js.map +1 -1
- package/dist/adapters/mysql/tools/admin/backup.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/admin/backup.js +3 -3
- package/dist/adapters/mysql/tools/admin/backup.js.map +1 -1
- package/dist/adapters/mysql/tools/admin/maintenance.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/admin/maintenance.js +5 -5
- package/dist/adapters/mysql/tools/admin/maintenance.js.map +1 -1
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.js +26 -5
- package/dist/adapters/mysql/tools/cluster/innodb-cluster.js.map +1 -1
- package/dist/adapters/mysql/tools/codemode/index.d.ts +38 -0
- package/dist/adapters/mysql/tools/codemode/index.d.ts.map +1 -0
- package/dist/adapters/mysql/tools/codemode/index.js +203 -0
- package/dist/adapters/mysql/tools/codemode/index.js.map +1 -0
- package/dist/adapters/mysql/tools/core.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/core.js +32 -20
- package/dist/adapters/mysql/tools/core.js.map +1 -1
- package/dist/adapters/mysql/tools/events.js +18 -6
- package/dist/adapters/mysql/tools/events.js.map +1 -1
- package/dist/adapters/mysql/tools/json/core.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/json/core.js +5 -5
- package/dist/adapters/mysql/tools/json/core.js.map +1 -1
- package/dist/adapters/mysql/tools/json/helpers.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/json/helpers.js +9 -3
- package/dist/adapters/mysql/tools/json/helpers.js.map +1 -1
- package/dist/adapters/mysql/tools/partitioning.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/partitioning.js +38 -6
- package/dist/adapters/mysql/tools/partitioning.js.map +1 -1
- package/dist/adapters/mysql/tools/performance/analysis.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/performance/analysis.js +67 -20
- package/dist/adapters/mysql/tools/performance/analysis.js.map +1 -1
- package/dist/adapters/mysql/tools/performance/optimization.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/performance/optimization.js +36 -6
- package/dist/adapters/mysql/tools/performance/optimization.js.map +1 -1
- package/dist/adapters/mysql/tools/security/data-protection.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/security/data-protection.js +9 -4
- package/dist/adapters/mysql/tools/security/data-protection.js.map +1 -1
- package/dist/adapters/mysql/tools/shell/common.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/shell/common.js +28 -2
- package/dist/adapters/mysql/tools/shell/common.js.map +1 -1
- package/dist/adapters/mysql/tools/shell/restore.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/shell/restore.js +54 -4
- package/dist/adapters/mysql/tools/shell/restore.js.map +1 -1
- package/dist/adapters/mysql/tools/spatial/operations.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/spatial/operations.js +10 -2
- package/dist/adapters/mysql/tools/spatial/operations.js.map +1 -1
- package/dist/adapters/mysql/tools/spatial/setup.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/spatial/setup.js +18 -0
- package/dist/adapters/mysql/tools/spatial/setup.js.map +1 -1
- package/dist/adapters/mysql/tools/sysschema/resources.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/sysschema/resources.js +5 -0
- package/dist/adapters/mysql/tools/sysschema/resources.js.map +1 -1
- package/dist/adapters/mysql/tools/text/fulltext.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/text/fulltext.js +6 -4
- package/dist/adapters/mysql/tools/text/fulltext.js.map +1 -1
- package/dist/adapters/mysql/tools/text/processing.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/text/processing.js +10 -45
- package/dist/adapters/mysql/tools/text/processing.js.map +1 -1
- package/dist/adapters/mysql/tools/transactions.d.ts.map +1 -1
- package/dist/adapters/mysql/tools/transactions.js +8 -8
- package/dist/adapters/mysql/tools/transactions.js.map +1 -1
- package/dist/adapters/mysql/types.d.ts +968 -78
- package/dist/adapters/mysql/types.d.ts.map +1 -1
- package/dist/adapters/mysql/types.js +1084 -78
- package/dist/adapters/mysql/types.js.map +1 -1
- package/dist/auth/scopes.d.ts.map +1 -1
- package/dist/auth/scopes.js +1 -0
- package/dist/auth/scopes.js.map +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +12 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/codemode/api.d.ts +69 -0
- package/dist/codemode/api.d.ts.map +1 -0
- package/dist/codemode/api.js +1035 -0
- package/dist/codemode/api.js.map +1 -0
- package/dist/codemode/index.d.ts +13 -0
- package/dist/codemode/index.d.ts.map +1 -0
- package/dist/codemode/index.js +17 -0
- package/dist/codemode/index.js.map +1 -0
- package/dist/codemode/sandbox-factory.d.ts +72 -0
- package/dist/codemode/sandbox-factory.d.ts.map +1 -0
- package/dist/codemode/sandbox-factory.js +88 -0
- package/dist/codemode/sandbox-factory.js.map +1 -0
- package/dist/codemode/sandbox.d.ts +96 -0
- package/dist/codemode/sandbox.d.ts.map +1 -0
- package/dist/codemode/sandbox.js +345 -0
- package/dist/codemode/sandbox.js.map +1 -0
- package/dist/codemode/security.d.ts +44 -0
- package/dist/codemode/security.d.ts.map +1 -0
- package/dist/codemode/security.js +149 -0
- package/dist/codemode/security.js.map +1 -0
- package/dist/codemode/types.d.ts +137 -0
- package/dist/codemode/types.d.ts.map +1 -0
- package/dist/codemode/types.js +46 -0
- package/dist/codemode/types.js.map +1 -0
- package/dist/codemode/worker-sandbox.d.ts +82 -0
- package/dist/codemode/worker-sandbox.d.ts.map +1 -0
- package/dist/codemode/worker-sandbox.js +244 -0
- package/dist/codemode/worker-sandbox.js.map +1 -0
- package/dist/codemode/worker-script.d.ts +8 -0
- package/dist/codemode/worker-script.d.ts.map +1 -0
- package/dist/codemode/worker-script.js +113 -0
- package/dist/codemode/worker-script.js.map +1 -0
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +33 -9
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/filtering/ToolConstants.d.ts +11 -11
- package/dist/filtering/ToolConstants.d.ts.map +1 -1
- package/dist/filtering/ToolConstants.js +37 -19
- package/dist/filtering/ToolConstants.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +12 -0
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/server/McpServer.js +1 -1
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/modules/server.d.ts +2 -0
- package/dist/types/modules/server.d.ts.map +1 -1
- package/dist/types/modules/tools.d.ts +1 -1
- package/dist/types/modules/tools.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +12 -7
- package/releases/v2.2.0-release-notes.md +18 -18
- package/releases/v2.3.0-release-notes.md +191 -0
- package/src/__tests__/perf.test.ts +12 -12
- package/src/adapters/mysql/MySQLAdapter.ts +10 -0
- package/src/adapters/mysql/__tests__/MySQLAdapter.test.ts +1 -1
- package/src/adapters/mysql/prompts/index.ts +8 -1
- package/src/adapters/mysql/prompts/routerSetup.ts +5 -0
- package/src/adapters/mysql/resources/__tests__/capabilities.test.ts +50 -1
- package/src/adapters/mysql/resources/capabilities.ts +6 -4
- package/src/adapters/mysql/resources/index.ts +9 -1
- package/src/adapters/mysql/tools/__tests__/core.test.ts +68 -0
- package/src/adapters/mysql/tools/__tests__/events.test.ts +56 -2
- package/src/adapters/mysql/tools/__tests__/json_core.test.ts +1 -1
- package/src/adapters/mysql/tools/__tests__/json_helpers.test.ts +46 -4
- package/src/adapters/mysql/tools/__tests__/replication.test.ts +144 -42
- package/src/adapters/mysql/tools/__tests__/security.test.ts +39 -0
- package/src/adapters/mysql/tools/__tests__/spatial.test.ts +39 -7
- package/src/adapters/mysql/tools/__tests__/spatial_handler.test.ts +35 -3
- package/src/adapters/mysql/tools/__tests__/transactions.test.ts +3 -5
- package/src/adapters/mysql/tools/admin/backup.ts +8 -3
- package/src/adapters/mysql/tools/admin/maintenance.ts +8 -4
- package/src/adapters/mysql/tools/cluster/__tests__/innodb-cluster.test.ts +35 -0
- package/src/adapters/mysql/tools/cluster/innodb-cluster.ts +26 -5
- package/src/adapters/mysql/tools/codemode/index.ts +249 -0
- package/src/adapters/mysql/tools/core.ts +44 -27
- package/src/adapters/mysql/tools/events.ts +23 -7
- package/src/adapters/mysql/tools/json/__tests__/helpers.test.ts +59 -14
- package/src/adapters/mysql/tools/json/core.ts +8 -4
- package/src/adapters/mysql/tools/json/helpers.ts +13 -3
- package/src/adapters/mysql/tools/partitioning.ts +53 -6
- package/src/adapters/mysql/tools/performance/__tests__/analysis.test.ts +227 -4
- package/src/adapters/mysql/tools/performance/__tests__/optimization.test.ts +35 -0
- package/src/adapters/mysql/tools/performance/analysis.ts +75 -21
- package/src/adapters/mysql/tools/performance/optimization.ts +44 -6
- package/src/adapters/mysql/tools/security/data-protection.ts +10 -4
- package/src/adapters/mysql/tools/shell/__tests__/common.test.ts +46 -0
- package/src/adapters/mysql/tools/shell/__tests__/restore.test.ts +28 -1
- package/src/adapters/mysql/tools/shell/common.ts +34 -2
- package/src/adapters/mysql/tools/shell/restore.ts +70 -7
- package/src/adapters/mysql/tools/spatial/__tests__/operations.test.ts +29 -0
- package/src/adapters/mysql/tools/spatial/operations.ts +13 -2
- package/src/adapters/mysql/tools/spatial/setup.ts +23 -0
- package/src/adapters/mysql/tools/sysschema/__tests__/resources.test.ts +21 -0
- package/src/adapters/mysql/tools/sysschema/resources.ts +5 -0
- package/src/adapters/mysql/tools/text/fulltext.ts +13 -5
- package/src/adapters/mysql/tools/text/processing.ts +20 -49
- package/src/adapters/mysql/tools/transactions.ts +11 -7
- package/src/adapters/mysql/types.ts +1241 -87
- package/src/auth/scopes.ts +1 -0
- package/src/cli/args.ts +14 -0
- package/src/codemode/api.ts +1224 -0
- package/src/codemode/index.ts +51 -0
- package/src/codemode/sandbox-factory.ts +146 -0
- package/src/codemode/sandbox.ts +450 -0
- package/src/codemode/security.ts +188 -0
- package/src/codemode/types.ts +194 -0
- package/src/codemode/worker-sandbox.ts +326 -0
- package/src/codemode/worker-script.ts +144 -0
- package/src/constants/ServerInstructions.ts +33 -9
- package/src/filtering/ToolConstants.ts +37 -19
- package/src/filtering/ToolFilter.ts +15 -0
- package/src/filtering/__tests__/ToolFilter.test.ts +65 -38
- package/src/server/McpServer.ts +1 -1
- package/src/types/modules/server.ts +3 -0
- package/src/types/modules/tools.ts +2 -1
- package/src/utils/logger.ts +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import * as child_process from "child_process";
|
|
3
|
+
import * as fsModule from "fs";
|
|
3
4
|
import {
|
|
4
5
|
createMockMySQLAdapter,
|
|
5
6
|
createMockRequestContext,
|
|
@@ -13,6 +14,19 @@ vi.mock("child_process", () => ({
|
|
|
13
14
|
spawn: vi.fn(),
|
|
14
15
|
}));
|
|
15
16
|
|
|
17
|
+
vi.mock("fs", async () => {
|
|
18
|
+
const actual = await vi.importActual<typeof import("fs")>("fs");
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
promises: {
|
|
22
|
+
...actual.promises,
|
|
23
|
+
mkdtemp: vi.fn().mockResolvedValue("/tmp/mysqlsh_script_abc123"),
|
|
24
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
rm: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
16
30
|
describe("Shell Restore and Script Tools", () => {
|
|
17
31
|
let mockAdapter: ReturnType<typeof createMockMySQLAdapter>;
|
|
18
32
|
let mockContext: ReturnType<typeof createMockRequestContext>;
|
|
@@ -226,7 +240,7 @@ describe("Shell Restore and Script Tools", () => {
|
|
|
226
240
|
expect(args).toContain("--py");
|
|
227
241
|
});
|
|
228
242
|
|
|
229
|
-
it("should run sql script", async () => {
|
|
243
|
+
it("should run sql script via secure temp file", async () => {
|
|
230
244
|
setupMockSpawn("SQL output");
|
|
231
245
|
|
|
232
246
|
const tool = createShellRunScriptTool();
|
|
@@ -241,6 +255,19 @@ describe("Shell Restore and Script Tools", () => {
|
|
|
241
255
|
expect(result.success).toBe(true);
|
|
242
256
|
const args = mockSpawn.mock.calls[0][1];
|
|
243
257
|
expect(args).toContain("--sql");
|
|
258
|
+
expect(args).toContain("--file");
|
|
259
|
+
|
|
260
|
+
// Verify secure temp dir was created and cleaned up
|
|
261
|
+
const fsp = fsModule.promises;
|
|
262
|
+
expect(fsp.mkdtemp).toHaveBeenCalled();
|
|
263
|
+
expect(fsp.writeFile).toHaveBeenCalledWith(
|
|
264
|
+
expect.stringContaining("script.sql"),
|
|
265
|
+
"SELECT 1",
|
|
266
|
+
"utf8",
|
|
267
|
+
);
|
|
268
|
+
expect(fsp.rm).toHaveBeenCalledWith("/tmp/mysqlsh_script_abc123", {
|
|
269
|
+
recursive: true,
|
|
270
|
+
});
|
|
244
271
|
});
|
|
245
272
|
});
|
|
246
273
|
});
|
|
@@ -196,8 +196,24 @@ export async function execShellJS(
|
|
|
196
196
|
}
|
|
197
197
|
// Fatal dump errors
|
|
198
198
|
if (stderrClean.includes("Fatal error during dump")) {
|
|
199
|
+
// Extract specific MySQL error lines (e.g., "ERROR: Unknown column 'x' in 'where clause'")
|
|
200
|
+
const errorLines = stderrClean
|
|
201
|
+
.split(/\r?\n/)
|
|
202
|
+
.filter((line) => /^ERROR:/i.test(line.trim()));
|
|
203
|
+
const specificError =
|
|
204
|
+
errorLines.length > 0
|
|
205
|
+
? errorLines
|
|
206
|
+
.map((line) => line.trim().replace(/^ERROR:\s*/i, ""))
|
|
207
|
+
.join("; ")
|
|
208
|
+
: null;
|
|
209
|
+
|
|
210
|
+
if (specificError) {
|
|
211
|
+
throw new Error(specificError);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fallback: no specific error extracted, use generic message with privilege hint
|
|
199
215
|
throw new Error(
|
|
200
|
-
`MySQL Shell dump failed:
|
|
216
|
+
`MySQL Shell dump failed: Fatal error during dump. ` +
|
|
201
217
|
`This may be caused by missing privileges. For dumpSchemas, try excludeEvents: true. ` +
|
|
202
218
|
`For dumpTables, try all: false.`,
|
|
203
219
|
);
|
|
@@ -223,7 +239,23 @@ export async function execShellJS(
|
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
if (!parsed.success) {
|
|
226
|
-
|
|
242
|
+
const errorMsg = parsed.error ?? "Unknown MySQL Shell error";
|
|
243
|
+
|
|
244
|
+
// For "Fatal error during dump" errors, check stderr for specific MySQL error details
|
|
245
|
+
if (errorMsg.includes("Fatal error during dump") && stderrClean) {
|
|
246
|
+
const errorLines = stderrClean
|
|
247
|
+
.split(/\r?\n/)
|
|
248
|
+
.filter((line) => /^ERROR:/i.test(line.trim()));
|
|
249
|
+
|
|
250
|
+
if (errorLines.length > 0) {
|
|
251
|
+
const specificError = errorLines
|
|
252
|
+
.map((line) => line.trim().replace(/^ERROR:\s*/i, ""))
|
|
253
|
+
.join("; ");
|
|
254
|
+
throw new Error(specificError);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(errorMsg);
|
|
227
259
|
}
|
|
228
260
|
return parsed.result;
|
|
229
261
|
}
|
|
@@ -94,11 +94,74 @@ export function createShellLoadDumpTool(): ToolDefinition {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
// For dry runs, use execMySQLShell directly to capture stderr
|
|
99
|
+
// where MySQL Shell outputs the summary of what would be loaded
|
|
100
|
+
const config = getShellConfig();
|
|
101
|
+
const dryRunJsCode = `
|
|
102
|
+
var __result__;
|
|
103
|
+
try {
|
|
104
|
+
__result__ = (function() { ${jsCode} })();
|
|
105
|
+
print(JSON.stringify({ success: true, result: __result__ }));
|
|
106
|
+
} catch (e) {
|
|
107
|
+
print(JSON.stringify({ success: false, error: e.message }));
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const rawResult = await execMySQLShell(
|
|
111
|
+
["--uri", config.connectionUri, "--js", "-e", dryRunJsCode],
|
|
112
|
+
{ timeout: 3600000 },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Parse stderr for dry run summary, filtering out common warnings
|
|
116
|
+
const stderrClean = rawResult.stderr
|
|
117
|
+
.replace(
|
|
118
|
+
/WARNING: Using a password on the command line interface can be insecure\.\s*/gi,
|
|
119
|
+
"",
|
|
120
|
+
)
|
|
121
|
+
.trim();
|
|
122
|
+
|
|
123
|
+
// Check for errors in the JSON output
|
|
124
|
+
const lines = rawResult.stdout.trim().split("\n");
|
|
125
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
126
|
+
const line = lines[i];
|
|
127
|
+
if (!line) continue;
|
|
128
|
+
const trimmedLine = line.trim();
|
|
129
|
+
if (trimmedLine.startsWith("{")) {
|
|
130
|
+
let parsed: {
|
|
131
|
+
success: boolean;
|
|
132
|
+
result?: unknown;
|
|
133
|
+
error?: string;
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(trimmedLine) as {
|
|
137
|
+
success: boolean;
|
|
138
|
+
result?: unknown;
|
|
139
|
+
error?: string;
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!parsed.success) {
|
|
145
|
+
throw new Error(parsed.error ?? "Unknown MySQL Shell error");
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
inputDir,
|
|
154
|
+
dryRun: true,
|
|
155
|
+
localInfileEnabled: updateServerSettings,
|
|
156
|
+
dryRunOutput: stderrClean || undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
97
160
|
const result = await execShellJS(jsCode, { timeout: 3600000 });
|
|
98
161
|
return {
|
|
99
162
|
success: true,
|
|
100
163
|
inputDir,
|
|
101
|
-
dryRun:
|
|
164
|
+
dryRun: false,
|
|
102
165
|
localInfileEnabled: updateServerSettings,
|
|
103
166
|
result,
|
|
104
167
|
};
|
|
@@ -169,10 +232,10 @@ export function createShellRunScriptTool(): ToolDefinition {
|
|
|
169
232
|
// SQL scripts with comments or multi-line content break when passed via -e
|
|
170
233
|
// Use --file approach for SQL to properly handle all syntax
|
|
171
234
|
if (language === "sql") {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
235
|
+
// Create a secure temp directory via mkdtemp (restrictive permissions,
|
|
236
|
+
// unique path) to avoid CodeQL js/insecure-temporary-file alert.
|
|
237
|
+
const tempDir = await fs.mkdtemp(join(tmpdir(), `mysqlsh_script_`));
|
|
238
|
+
const tempFile = join(tempDir, "script.sql");
|
|
176
239
|
try {
|
|
177
240
|
await fs.writeFile(tempFile, script, "utf8");
|
|
178
241
|
const args = [
|
|
@@ -184,8 +247,8 @@ export function createShellRunScriptTool(): ToolDefinition {
|
|
|
184
247
|
];
|
|
185
248
|
result = await execMySQLShell(args, { timeout });
|
|
186
249
|
} finally {
|
|
187
|
-
// Cleanup temp
|
|
188
|
-
await fs.
|
|
250
|
+
// Cleanup temp directory and its contents
|
|
251
|
+
await fs.rm(tempDir, { recursive: true }).catch(() => void 0);
|
|
189
252
|
}
|
|
190
253
|
} else {
|
|
191
254
|
// JS and Python work fine with -e
|
|
@@ -123,6 +123,7 @@ describe("Spatial Operations Tools", () => {
|
|
|
123
123
|
expect(result).toHaveProperty("bufferWkt");
|
|
124
124
|
expect(result).toHaveProperty("bufferGeoJson");
|
|
125
125
|
expect(result).toHaveProperty("segments", 8);
|
|
126
|
+
expect(result).toHaveProperty("precision", 6);
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
it("should use ST_Buffer_Strategy with Cartesian SRID", async () => {
|
|
@@ -151,6 +152,34 @@ describe("Spatial Operations Tools", () => {
|
|
|
151
152
|
const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
|
|
152
153
|
expect(call).toContain("ST_Buffer_Strategy('point_circle', 4)");
|
|
153
154
|
expect(result).toHaveProperty("segments", 4);
|
|
155
|
+
expect(result).toHaveProperty("precision", 6);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should pass custom precision to ST_AsGeoJSON", async () => {
|
|
159
|
+
mockAdapter.executeQuery.mockResolvedValue(
|
|
160
|
+
createMockQueryResult([
|
|
161
|
+
{
|
|
162
|
+
buffer_wkt: "POLYGON(...)",
|
|
163
|
+
buffer_geojson: '{"type":"Polygon"}',
|
|
164
|
+
},
|
|
165
|
+
]),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const tool = createSpatialBufferTool(
|
|
169
|
+
mockAdapter as unknown as MySQLAdapter,
|
|
170
|
+
);
|
|
171
|
+
const result = await tool.handler(
|
|
172
|
+
{
|
|
173
|
+
geometry: "POINT(-73.9857 40.7484)",
|
|
174
|
+
distance: 1000,
|
|
175
|
+
precision: 2,
|
|
176
|
+
},
|
|
177
|
+
mockContext,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
|
|
181
|
+
expect(call).toContain(", 2) as buffer_geojson");
|
|
182
|
+
expect(result).toHaveProperty("precision", 2);
|
|
154
183
|
});
|
|
155
184
|
});
|
|
156
185
|
|
|
@@ -60,6 +60,15 @@ const BufferSchema = z.object({
|
|
|
60
60
|
.describe(
|
|
61
61
|
"Number of segments per quarter-circle for buffer polygon approximation (default: 8, MySQL default: 32). Lower values produce simpler polygons with smaller payloads. Only effective with Cartesian geometries (SRID 0); geographic SRIDs use MySQL's internal algorithm.",
|
|
62
62
|
),
|
|
63
|
+
precision: z
|
|
64
|
+
.number()
|
|
65
|
+
.int()
|
|
66
|
+
.min(0)
|
|
67
|
+
.max(15)
|
|
68
|
+
.default(6)
|
|
69
|
+
.describe(
|
|
70
|
+
"Decimal precision for GeoJSON output coordinates (default: 6, ~0.11m accuracy). Lower values reduce payload size.",
|
|
71
|
+
),
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
const TransformSchema = z.object({
|
|
@@ -152,7 +161,8 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
152
161
|
idempotentHint: true,
|
|
153
162
|
},
|
|
154
163
|
handler: async (params: unknown, _context: RequestContext) => {
|
|
155
|
-
const { geometry, distance, srid, segments } =
|
|
164
|
+
const { geometry, distance, srid, segments, precision } =
|
|
165
|
+
BufferSchema.parse(params);
|
|
156
166
|
|
|
157
167
|
try {
|
|
158
168
|
// ST_Buffer_Strategy only works with Cartesian (non-geographic) SRIDs.
|
|
@@ -164,7 +174,7 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
164
174
|
const result = await adapter.executeQuery(
|
|
165
175
|
`SELECT
|
|
166
176
|
ST_AsText(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause})) as buffer_wkt,
|
|
167
|
-
ST_AsGeoJSON(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause})) as buffer_geojson`,
|
|
177
|
+
ST_AsGeoJSON(ST_Buffer(ST_GeomFromText(?, ${String(srid)}, 'axis-order=long-lat'), ?${strategyClause}), ${String(precision)}) as buffer_geojson`,
|
|
168
178
|
[geometry, distance, geometry, distance],
|
|
169
179
|
);
|
|
170
180
|
|
|
@@ -175,6 +185,7 @@ export function createSpatialBufferTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
175
185
|
bufferDistance: distance,
|
|
176
186
|
segments,
|
|
177
187
|
segmentsApplied: !isGeographic,
|
|
188
|
+
precision,
|
|
178
189
|
srid,
|
|
179
190
|
};
|
|
180
191
|
} catch (error) {
|
|
@@ -163,6 +163,23 @@ export function createSpatialCreateIndexTool(
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// Check if a SPATIAL index already exists on this column (any name)
|
|
167
|
+
const existingIdx = await adapter.executeQuery(
|
|
168
|
+
`SELECT INDEX_NAME FROM information_schema.STATISTICS
|
|
169
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? AND INDEX_TYPE = 'SPATIAL'
|
|
170
|
+
LIMIT 1`,
|
|
171
|
+
[table, column],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const existingRow = existingIdx.rows?.[0];
|
|
175
|
+
if (existingRow) {
|
|
176
|
+
const existingName = String(existingRow["INDEX_NAME"]);
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
reason: `Spatial index '${existingName}' already exists on column '${column}' of table '${table}'`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
await adapter.executeQuery(
|
|
167
184
|
`CREATE SPATIAL INDEX \`${idxName}\` ON \`${table}\`(\`${column}\`)`,
|
|
168
185
|
);
|
|
@@ -181,6 +198,12 @@ export function createSpatialCreateIndexTool(
|
|
|
181
198
|
if (msg.includes("Cannot create SPATIAL index on nullable column")) {
|
|
182
199
|
return { success: false, reason: msg };
|
|
183
200
|
}
|
|
201
|
+
if (msg.includes("Duplicate key name")) {
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
reason: `Index '${idxName}' already exists on table '${table}'`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
184
207
|
return { success: false, error: msg };
|
|
185
208
|
}
|
|
186
209
|
},
|
|
@@ -52,6 +52,9 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
52
52
|
tableStatistics: unknown[];
|
|
53
53
|
indexStatistics: unknown[];
|
|
54
54
|
autoIncrementStatus: unknown[];
|
|
55
|
+
tableStatisticsCount: number;
|
|
56
|
+
indexStatisticsCount: number;
|
|
57
|
+
autoIncrementStatusCount: number;
|
|
55
58
|
schemaName: string;
|
|
56
59
|
};
|
|
57
60
|
|
|
@@ -59,6 +62,9 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
59
62
|
expect(result.tableStatistics).toHaveLength(1);
|
|
60
63
|
expect(result.indexStatistics).toHaveLength(1);
|
|
61
64
|
expect(result.autoIncrementStatus).toHaveLength(1);
|
|
65
|
+
expect(result.tableStatisticsCount).toBe(1);
|
|
66
|
+
expect(result.indexStatisticsCount).toBe(1);
|
|
67
|
+
expect(result.autoIncrementStatusCount).toBe(1);
|
|
62
68
|
expect(result.schemaName).toBe("testdb");
|
|
63
69
|
});
|
|
64
70
|
|
|
@@ -113,6 +119,9 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
113
119
|
expect(result.tableStatistics).toEqual([]);
|
|
114
120
|
expect(result.indexStatistics).toEqual([]);
|
|
115
121
|
expect(result.autoIncrementStatus).toEqual([]);
|
|
122
|
+
expect(result.tableStatisticsCount).toBe(0);
|
|
123
|
+
expect(result.indexStatisticsCount).toBe(0);
|
|
124
|
+
expect(result.autoIncrementStatusCount).toBe(0);
|
|
116
125
|
});
|
|
117
126
|
|
|
118
127
|
it("should return exists: false for nonexistent schema (P154)", async () => {
|
|
@@ -144,9 +153,15 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
144
153
|
);
|
|
145
154
|
const result = (await tool.handler({}, mockContext)) as {
|
|
146
155
|
schemaName: string;
|
|
156
|
+
tableStatisticsCount: number;
|
|
157
|
+
indexStatisticsCount: number;
|
|
158
|
+
autoIncrementStatusCount: number;
|
|
147
159
|
};
|
|
148
160
|
|
|
149
161
|
expect(result.schemaName).toBe("real_db_name");
|
|
162
|
+
expect(result.tableStatisticsCount).toBe(0);
|
|
163
|
+
expect(result.indexStatisticsCount).toBe(0);
|
|
164
|
+
expect(result.autoIncrementStatusCount).toBe(0);
|
|
150
165
|
// First call should be SELECT DATABASE()
|
|
151
166
|
const firstCall = mockAdapter.executeQuery.mock.calls[0][0] as string;
|
|
152
167
|
expect(firstCall).toContain("SELECT DATABASE()");
|
|
@@ -235,11 +250,15 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
235
250
|
const result = (await tool.handler({}, mockContext)) as {
|
|
236
251
|
globalMemory: unknown[];
|
|
237
252
|
memoryByUser: unknown[];
|
|
253
|
+
globalMemoryCount: number;
|
|
254
|
+
memoryByUserCount: number;
|
|
238
255
|
};
|
|
239
256
|
|
|
240
257
|
expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
|
|
241
258
|
expect(result.globalMemory).toHaveLength(1);
|
|
242
259
|
expect(result.memoryByUser).toHaveLength(1);
|
|
260
|
+
expect(result.globalMemoryCount).toBe(1);
|
|
261
|
+
expect(result.memoryByUserCount).toBe(1);
|
|
243
262
|
});
|
|
244
263
|
|
|
245
264
|
it("should handle null rows", async () => {
|
|
@@ -255,6 +274,8 @@ describe("Sys Schema Resource Tools", () => {
|
|
|
255
274
|
|
|
256
275
|
expect(result.globalMemory).toEqual([]);
|
|
257
276
|
expect(result.memoryByUser).toEqual([]);
|
|
277
|
+
expect(result.globalMemoryCount).toBe(0);
|
|
278
|
+
expect(result.memoryByUserCount).toBe(0);
|
|
258
279
|
});
|
|
259
280
|
});
|
|
260
281
|
});
|
|
@@ -142,6 +142,9 @@ export function createSysSchemaStatsTool(
|
|
|
142
142
|
tableStatistics: tableStats.rows ?? [],
|
|
143
143
|
indexStatistics: indexStats.rows ?? [],
|
|
144
144
|
autoIncrementStatus: autoIncStats.rows ?? [],
|
|
145
|
+
tableStatisticsCount: (tableStats.rows ?? []).length,
|
|
146
|
+
indexStatisticsCount: (indexStats.rows ?? []).length,
|
|
147
|
+
autoIncrementStatusCount: (autoIncStats.rows ?? []).length,
|
|
145
148
|
schemaName: resolvedSchema,
|
|
146
149
|
};
|
|
147
150
|
},
|
|
@@ -262,6 +265,8 @@ export function createSysMemorySummaryTool(
|
|
|
262
265
|
return {
|
|
263
266
|
globalMemory: globalStats.rows ?? [],
|
|
264
267
|
memoryByUser: userStats.rows ?? [],
|
|
268
|
+
globalMemoryCount: (globalStats.rows ?? []).length,
|
|
269
|
+
memoryByUserCount: (userStats.rows ?? []).length,
|
|
265
270
|
};
|
|
266
271
|
},
|
|
267
272
|
};
|
|
@@ -10,7 +10,12 @@ import type {
|
|
|
10
10
|
ToolDefinition,
|
|
11
11
|
RequestContext,
|
|
12
12
|
} from "../../../../types/index.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
FulltextCreateSchema,
|
|
15
|
+
FulltextCreateSchemaBase,
|
|
16
|
+
FulltextSearchSchema,
|
|
17
|
+
FulltextSearchSchemaBase,
|
|
18
|
+
} from "../../types.js";
|
|
14
19
|
import { z } from "zod";
|
|
15
20
|
import {
|
|
16
21
|
validateIdentifier,
|
|
@@ -75,7 +80,7 @@ export function createFulltextCreateTool(
|
|
|
75
80
|
description:
|
|
76
81
|
"Create a FULLTEXT index on specified columns for fast text search.",
|
|
77
82
|
group: "fulltext",
|
|
78
|
-
inputSchema:
|
|
83
|
+
inputSchema: FulltextCreateSchemaBase,
|
|
79
84
|
requiredScopes: ["write"],
|
|
80
85
|
annotations: {
|
|
81
86
|
readOnlyHint: false,
|
|
@@ -156,7 +161,7 @@ export function createFulltextDropTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
156
161
|
};
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
const FulltextSearchWithTruncateSchema =
|
|
164
|
+
const FulltextSearchWithTruncateSchema = FulltextSearchSchemaBase.extend({
|
|
160
165
|
maxLength: z
|
|
161
166
|
.number()
|
|
162
167
|
.optional()
|
|
@@ -180,8 +185,11 @@ export function createFulltextSearchTool(
|
|
|
180
185
|
idempotentHint: true,
|
|
181
186
|
},
|
|
182
187
|
handler: async (params: unknown, _context: RequestContext) => {
|
|
183
|
-
const
|
|
184
|
-
|
|
188
|
+
const parsed = FulltextSearchSchema.parse(params);
|
|
189
|
+
const { table, columns, query, mode } = parsed;
|
|
190
|
+
const maxLength = (params as Record<string, unknown>)["maxLength"] as
|
|
191
|
+
| number
|
|
192
|
+
| undefined;
|
|
185
193
|
|
|
186
194
|
// Validate inputs
|
|
187
195
|
validateQualifiedIdentifier(table, "table");
|
|
@@ -12,10 +12,18 @@ import type {
|
|
|
12
12
|
} from "../../../../types/index.js";
|
|
13
13
|
import {
|
|
14
14
|
RegexpMatchSchema,
|
|
15
|
+
RegexpMatchSchemaBase,
|
|
15
16
|
LikeSearchSchema,
|
|
17
|
+
LikeSearchSchemaBase,
|
|
16
18
|
SoundexSchema,
|
|
19
|
+
SoundexSchemaBase,
|
|
20
|
+
SubstringSchema,
|
|
21
|
+
SubstringSchemaBase,
|
|
22
|
+
ConcatSchema,
|
|
23
|
+
ConcatSchemaBase,
|
|
24
|
+
CollationConvertSchema,
|
|
25
|
+
CollationConvertSchemaBase,
|
|
17
26
|
} from "../../types.js";
|
|
18
|
-
import { z } from "zod";
|
|
19
27
|
import {
|
|
20
28
|
validateIdentifier,
|
|
21
29
|
validateQualifiedIdentifier,
|
|
@@ -29,7 +37,7 @@ export function createRegexpMatchTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
29
37
|
title: "MySQL REGEXP Match",
|
|
30
38
|
description: "Find rows where column matches a regular expression pattern.",
|
|
31
39
|
group: "text",
|
|
32
|
-
inputSchema:
|
|
40
|
+
inputSchema: RegexpMatchSchemaBase,
|
|
33
41
|
requiredScopes: ["read"],
|
|
34
42
|
annotations: {
|
|
35
43
|
readOnlyHint: true,
|
|
@@ -70,7 +78,7 @@ export function createLikeSearchTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
70
78
|
description:
|
|
71
79
|
"Find rows using LIKE pattern matching with % and _ wildcards.",
|
|
72
80
|
group: "text",
|
|
73
|
-
inputSchema:
|
|
81
|
+
inputSchema: LikeSearchSchemaBase,
|
|
74
82
|
requiredScopes: ["read"],
|
|
75
83
|
annotations: {
|
|
76
84
|
readOnlyHint: true,
|
|
@@ -110,7 +118,7 @@ export function createSoundexTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
110
118
|
title: "MySQL SOUNDEX",
|
|
111
119
|
description: "Find rows with phonetically similar values using SOUNDEX.",
|
|
112
120
|
group: "text",
|
|
113
|
-
inputSchema:
|
|
121
|
+
inputSchema: SoundexSchemaBase,
|
|
114
122
|
requiredScopes: ["read"],
|
|
115
123
|
annotations: {
|
|
116
124
|
readOnlyHint: true,
|
|
@@ -145,27 +153,20 @@ export function createSoundexTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
145
153
|
}
|
|
146
154
|
|
|
147
155
|
export function createSubstringTool(adapter: MySQLAdapter): ToolDefinition {
|
|
148
|
-
const schema = z.object({
|
|
149
|
-
table: z.string(),
|
|
150
|
-
column: z.string(),
|
|
151
|
-
start: z.number().describe("Starting position (1-indexed)"),
|
|
152
|
-
length: z.number().optional().describe("Number of characters"),
|
|
153
|
-
where: z.string().optional(),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
156
|
return {
|
|
157
157
|
name: "mysql_substring",
|
|
158
158
|
title: "MySQL SUBSTRING",
|
|
159
159
|
description: "Extract substrings from column values.",
|
|
160
160
|
group: "text",
|
|
161
|
-
inputSchema:
|
|
161
|
+
inputSchema: SubstringSchemaBase,
|
|
162
162
|
requiredScopes: ["read"],
|
|
163
163
|
annotations: {
|
|
164
164
|
readOnlyHint: true,
|
|
165
165
|
idempotentHint: true,
|
|
166
166
|
},
|
|
167
167
|
handler: async (params: unknown, _context: RequestContext) => {
|
|
168
|
-
const { table, column, start, length, where } =
|
|
168
|
+
const { table, column, start, length, where } =
|
|
169
|
+
SubstringSchema.parse(params);
|
|
169
170
|
|
|
170
171
|
// Validate inputs
|
|
171
172
|
validateQualifiedIdentifier(table, "table");
|
|
@@ -201,35 +202,12 @@ export function createSubstringTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
|
|
204
|
-
const schema = z.object({
|
|
205
|
-
table: z.string(),
|
|
206
|
-
columns: z.array(z.string()).describe("Columns to concatenate"),
|
|
207
|
-
separator: z
|
|
208
|
-
.string()
|
|
209
|
-
.optional()
|
|
210
|
-
.default(" ")
|
|
211
|
-
.describe("Separator between values"),
|
|
212
|
-
alias: z
|
|
213
|
-
.string()
|
|
214
|
-
.optional()
|
|
215
|
-
.default("concatenated")
|
|
216
|
-
.describe("Result column name"),
|
|
217
|
-
where: z.string().optional(),
|
|
218
|
-
includeSourceColumns: z
|
|
219
|
-
.boolean()
|
|
220
|
-
.optional()
|
|
221
|
-
.default(true)
|
|
222
|
-
.describe(
|
|
223
|
-
"Include individual source columns in output (default: true). Set to false for minimal payload.",
|
|
224
|
-
),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
205
|
return {
|
|
228
206
|
name: "mysql_concat",
|
|
229
207
|
title: "MySQL CONCAT",
|
|
230
208
|
description: "Concatenate multiple columns with an optional separator.",
|
|
231
209
|
group: "text",
|
|
232
|
-
inputSchema:
|
|
210
|
+
inputSchema: ConcatSchemaBase,
|
|
233
211
|
requiredScopes: ["read"],
|
|
234
212
|
annotations: {
|
|
235
213
|
readOnlyHint: true,
|
|
@@ -237,7 +215,7 @@ export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
237
215
|
},
|
|
238
216
|
handler: async (params: unknown, _context: RequestContext) => {
|
|
239
217
|
const { table, columns, separator, alias, where, includeSourceColumns } =
|
|
240
|
-
|
|
218
|
+
ConcatSchema.parse(params);
|
|
241
219
|
|
|
242
220
|
// Validate inputs
|
|
243
221
|
validateQualifiedIdentifier(table, "table");
|
|
@@ -278,28 +256,21 @@ export function createConcatTool(adapter: MySQLAdapter): ToolDefinition {
|
|
|
278
256
|
export function createCollationConvertTool(
|
|
279
257
|
adapter: MySQLAdapter,
|
|
280
258
|
): ToolDefinition {
|
|
281
|
-
const schema = z.object({
|
|
282
|
-
table: z.string(),
|
|
283
|
-
column: z.string(),
|
|
284
|
-
charset: z.string().describe("Target character set (e.g., utf8mb4)"),
|
|
285
|
-
collation: z.string().optional().describe("Target collation"),
|
|
286
|
-
where: z.string().optional(),
|
|
287
|
-
});
|
|
288
|
-
|
|
289
259
|
return {
|
|
290
260
|
name: "mysql_collation_convert",
|
|
291
261
|
title: "MySQL Collation Convert",
|
|
292
262
|
description:
|
|
293
263
|
"Convert column values to a different character set or collation.",
|
|
294
264
|
group: "text",
|
|
295
|
-
inputSchema:
|
|
265
|
+
inputSchema: CollationConvertSchemaBase,
|
|
296
266
|
requiredScopes: ["read"],
|
|
297
267
|
annotations: {
|
|
298
268
|
readOnlyHint: true,
|
|
299
269
|
idempotentHint: true,
|
|
300
270
|
},
|
|
301
271
|
handler: async (params: unknown, _context: RequestContext) => {
|
|
302
|
-
const { table, column, charset, collation, where } =
|
|
272
|
+
const { table, column, charset, collation, where } =
|
|
273
|
+
CollationConvertSchema.parse(params);
|
|
303
274
|
|
|
304
275
|
// Validate inputs
|
|
305
276
|
validateQualifiedIdentifier(table, "table");
|