@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.
Files changed (213) hide show
  1. package/.github/workflows/docker-publish.yml +1 -2
  2. package/CHANGELOG.md +85 -0
  3. package/CODE_MODE.md +245 -0
  4. package/DOCKER_README.md +59 -36
  5. package/README.md +65 -42
  6. package/VERSION +1 -1
  7. package/dist/adapters/mysql/MySQLAdapter.d.ts +4 -0
  8. package/dist/adapters/mysql/MySQLAdapter.d.ts.map +1 -1
  9. package/dist/adapters/mysql/MySQLAdapter.js +9 -0
  10. package/dist/adapters/mysql/MySQLAdapter.js.map +1 -1
  11. package/dist/adapters/mysql/prompts/index.d.ts +8 -1
  12. package/dist/adapters/mysql/prompts/index.d.ts.map +1 -1
  13. package/dist/adapters/mysql/prompts/index.js +8 -1
  14. package/dist/adapters/mysql/prompts/index.js.map +1 -1
  15. package/dist/adapters/mysql/prompts/routerSetup.d.ts.map +1 -1
  16. package/dist/adapters/mysql/prompts/routerSetup.js +5 -0
  17. package/dist/adapters/mysql/prompts/routerSetup.js.map +1 -1
  18. package/dist/adapters/mysql/resources/capabilities.d.ts.map +1 -1
  19. package/dist/adapters/mysql/resources/capabilities.js +6 -5
  20. package/dist/adapters/mysql/resources/capabilities.js.map +1 -1
  21. package/dist/adapters/mysql/resources/index.d.ts +9 -1
  22. package/dist/adapters/mysql/resources/index.d.ts.map +1 -1
  23. package/dist/adapters/mysql/resources/index.js +9 -1
  24. package/dist/adapters/mysql/resources/index.js.map +1 -1
  25. package/dist/adapters/mysql/tools/admin/backup.d.ts.map +1 -1
  26. package/dist/adapters/mysql/tools/admin/backup.js +3 -3
  27. package/dist/adapters/mysql/tools/admin/backup.js.map +1 -1
  28. package/dist/adapters/mysql/tools/admin/maintenance.d.ts.map +1 -1
  29. package/dist/adapters/mysql/tools/admin/maintenance.js +5 -5
  30. package/dist/adapters/mysql/tools/admin/maintenance.js.map +1 -1
  31. package/dist/adapters/mysql/tools/cluster/innodb-cluster.d.ts.map +1 -1
  32. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js +26 -5
  33. package/dist/adapters/mysql/tools/cluster/innodb-cluster.js.map +1 -1
  34. package/dist/adapters/mysql/tools/codemode/index.d.ts +38 -0
  35. package/dist/adapters/mysql/tools/codemode/index.d.ts.map +1 -0
  36. package/dist/adapters/mysql/tools/codemode/index.js +203 -0
  37. package/dist/adapters/mysql/tools/codemode/index.js.map +1 -0
  38. package/dist/adapters/mysql/tools/core.d.ts.map +1 -1
  39. package/dist/adapters/mysql/tools/core.js +32 -20
  40. package/dist/adapters/mysql/tools/core.js.map +1 -1
  41. package/dist/adapters/mysql/tools/events.js +18 -6
  42. package/dist/adapters/mysql/tools/events.js.map +1 -1
  43. package/dist/adapters/mysql/tools/json/core.d.ts.map +1 -1
  44. package/dist/adapters/mysql/tools/json/core.js +5 -5
  45. package/dist/adapters/mysql/tools/json/core.js.map +1 -1
  46. package/dist/adapters/mysql/tools/json/helpers.d.ts.map +1 -1
  47. package/dist/adapters/mysql/tools/json/helpers.js +9 -3
  48. package/dist/adapters/mysql/tools/json/helpers.js.map +1 -1
  49. package/dist/adapters/mysql/tools/partitioning.d.ts.map +1 -1
  50. package/dist/adapters/mysql/tools/partitioning.js +38 -6
  51. package/dist/adapters/mysql/tools/partitioning.js.map +1 -1
  52. package/dist/adapters/mysql/tools/performance/analysis.d.ts.map +1 -1
  53. package/dist/adapters/mysql/tools/performance/analysis.js +67 -20
  54. package/dist/adapters/mysql/tools/performance/analysis.js.map +1 -1
  55. package/dist/adapters/mysql/tools/performance/optimization.d.ts.map +1 -1
  56. package/dist/adapters/mysql/tools/performance/optimization.js +36 -6
  57. package/dist/adapters/mysql/tools/performance/optimization.js.map +1 -1
  58. package/dist/adapters/mysql/tools/security/data-protection.d.ts.map +1 -1
  59. package/dist/adapters/mysql/tools/security/data-protection.js +9 -4
  60. package/dist/adapters/mysql/tools/security/data-protection.js.map +1 -1
  61. package/dist/adapters/mysql/tools/shell/common.d.ts.map +1 -1
  62. package/dist/adapters/mysql/tools/shell/common.js +28 -2
  63. package/dist/adapters/mysql/tools/shell/common.js.map +1 -1
  64. package/dist/adapters/mysql/tools/shell/restore.d.ts.map +1 -1
  65. package/dist/adapters/mysql/tools/shell/restore.js +54 -4
  66. package/dist/adapters/mysql/tools/shell/restore.js.map +1 -1
  67. package/dist/adapters/mysql/tools/spatial/operations.d.ts.map +1 -1
  68. package/dist/adapters/mysql/tools/spatial/operations.js +10 -2
  69. package/dist/adapters/mysql/tools/spatial/operations.js.map +1 -1
  70. package/dist/adapters/mysql/tools/spatial/setup.d.ts.map +1 -1
  71. package/dist/adapters/mysql/tools/spatial/setup.js +18 -0
  72. package/dist/adapters/mysql/tools/spatial/setup.js.map +1 -1
  73. package/dist/adapters/mysql/tools/sysschema/resources.d.ts.map +1 -1
  74. package/dist/adapters/mysql/tools/sysschema/resources.js +5 -0
  75. package/dist/adapters/mysql/tools/sysschema/resources.js.map +1 -1
  76. package/dist/adapters/mysql/tools/text/fulltext.d.ts.map +1 -1
  77. package/dist/adapters/mysql/tools/text/fulltext.js +6 -4
  78. package/dist/adapters/mysql/tools/text/fulltext.js.map +1 -1
  79. package/dist/adapters/mysql/tools/text/processing.d.ts.map +1 -1
  80. package/dist/adapters/mysql/tools/text/processing.js +10 -45
  81. package/dist/adapters/mysql/tools/text/processing.js.map +1 -1
  82. package/dist/adapters/mysql/tools/transactions.d.ts.map +1 -1
  83. package/dist/adapters/mysql/tools/transactions.js +8 -8
  84. package/dist/adapters/mysql/tools/transactions.js.map +1 -1
  85. package/dist/adapters/mysql/types.d.ts +968 -78
  86. package/dist/adapters/mysql/types.d.ts.map +1 -1
  87. package/dist/adapters/mysql/types.js +1084 -78
  88. package/dist/adapters/mysql/types.js.map +1 -1
  89. package/dist/auth/scopes.d.ts.map +1 -1
  90. package/dist/auth/scopes.js +1 -0
  91. package/dist/auth/scopes.js.map +1 -1
  92. package/dist/cli/args.d.ts.map +1 -1
  93. package/dist/cli/args.js +12 -0
  94. package/dist/cli/args.js.map +1 -1
  95. package/dist/codemode/api.d.ts +69 -0
  96. package/dist/codemode/api.d.ts.map +1 -0
  97. package/dist/codemode/api.js +1035 -0
  98. package/dist/codemode/api.js.map +1 -0
  99. package/dist/codemode/index.d.ts +13 -0
  100. package/dist/codemode/index.d.ts.map +1 -0
  101. package/dist/codemode/index.js +17 -0
  102. package/dist/codemode/index.js.map +1 -0
  103. package/dist/codemode/sandbox-factory.d.ts +72 -0
  104. package/dist/codemode/sandbox-factory.d.ts.map +1 -0
  105. package/dist/codemode/sandbox-factory.js +88 -0
  106. package/dist/codemode/sandbox-factory.js.map +1 -0
  107. package/dist/codemode/sandbox.d.ts +96 -0
  108. package/dist/codemode/sandbox.d.ts.map +1 -0
  109. package/dist/codemode/sandbox.js +345 -0
  110. package/dist/codemode/sandbox.js.map +1 -0
  111. package/dist/codemode/security.d.ts +44 -0
  112. package/dist/codemode/security.d.ts.map +1 -0
  113. package/dist/codemode/security.js +149 -0
  114. package/dist/codemode/security.js.map +1 -0
  115. package/dist/codemode/types.d.ts +137 -0
  116. package/dist/codemode/types.d.ts.map +1 -0
  117. package/dist/codemode/types.js +46 -0
  118. package/dist/codemode/types.js.map +1 -0
  119. package/dist/codemode/worker-sandbox.d.ts +82 -0
  120. package/dist/codemode/worker-sandbox.d.ts.map +1 -0
  121. package/dist/codemode/worker-sandbox.js +244 -0
  122. package/dist/codemode/worker-sandbox.js.map +1 -0
  123. package/dist/codemode/worker-script.d.ts +8 -0
  124. package/dist/codemode/worker-script.d.ts.map +1 -0
  125. package/dist/codemode/worker-script.js +113 -0
  126. package/dist/codemode/worker-script.js.map +1 -0
  127. package/dist/constants/ServerInstructions.d.ts +1 -1
  128. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  129. package/dist/constants/ServerInstructions.js +33 -9
  130. package/dist/constants/ServerInstructions.js.map +1 -1
  131. package/dist/filtering/ToolConstants.d.ts +11 -11
  132. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  133. package/dist/filtering/ToolConstants.js +37 -19
  134. package/dist/filtering/ToolConstants.js.map +1 -1
  135. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  136. package/dist/filtering/ToolFilter.js +12 -0
  137. package/dist/filtering/ToolFilter.js.map +1 -1
  138. package/dist/server/McpServer.js +1 -1
  139. package/dist/server/McpServer.js.map +1 -1
  140. package/dist/types/modules/server.d.ts +2 -0
  141. package/dist/types/modules/server.d.ts.map +1 -1
  142. package/dist/types/modules/tools.d.ts +1 -1
  143. package/dist/types/modules/tools.d.ts.map +1 -1
  144. package/dist/utils/logger.d.ts +1 -1
  145. package/dist/utils/logger.d.ts.map +1 -1
  146. package/dist/utils/logger.js.map +1 -1
  147. package/package.json +12 -7
  148. package/releases/v2.2.0-release-notes.md +18 -18
  149. package/releases/v2.3.0-release-notes.md +191 -0
  150. package/src/__tests__/perf.test.ts +12 -12
  151. package/src/adapters/mysql/MySQLAdapter.ts +10 -0
  152. package/src/adapters/mysql/__tests__/MySQLAdapter.test.ts +1 -1
  153. package/src/adapters/mysql/prompts/index.ts +8 -1
  154. package/src/adapters/mysql/prompts/routerSetup.ts +5 -0
  155. package/src/adapters/mysql/resources/__tests__/capabilities.test.ts +50 -1
  156. package/src/adapters/mysql/resources/capabilities.ts +6 -4
  157. package/src/adapters/mysql/resources/index.ts +9 -1
  158. package/src/adapters/mysql/tools/__tests__/core.test.ts +68 -0
  159. package/src/adapters/mysql/tools/__tests__/events.test.ts +56 -2
  160. package/src/adapters/mysql/tools/__tests__/json_core.test.ts +1 -1
  161. package/src/adapters/mysql/tools/__tests__/json_helpers.test.ts +46 -4
  162. package/src/adapters/mysql/tools/__tests__/replication.test.ts +144 -42
  163. package/src/adapters/mysql/tools/__tests__/security.test.ts +39 -0
  164. package/src/adapters/mysql/tools/__tests__/spatial.test.ts +39 -7
  165. package/src/adapters/mysql/tools/__tests__/spatial_handler.test.ts +35 -3
  166. package/src/adapters/mysql/tools/__tests__/transactions.test.ts +3 -5
  167. package/src/adapters/mysql/tools/admin/backup.ts +8 -3
  168. package/src/adapters/mysql/tools/admin/maintenance.ts +8 -4
  169. package/src/adapters/mysql/tools/cluster/__tests__/innodb-cluster.test.ts +35 -0
  170. package/src/adapters/mysql/tools/cluster/innodb-cluster.ts +26 -5
  171. package/src/adapters/mysql/tools/codemode/index.ts +249 -0
  172. package/src/adapters/mysql/tools/core.ts +44 -27
  173. package/src/adapters/mysql/tools/events.ts +23 -7
  174. package/src/adapters/mysql/tools/json/__tests__/helpers.test.ts +59 -14
  175. package/src/adapters/mysql/tools/json/core.ts +8 -4
  176. package/src/adapters/mysql/tools/json/helpers.ts +13 -3
  177. package/src/adapters/mysql/tools/partitioning.ts +53 -6
  178. package/src/adapters/mysql/tools/performance/__tests__/analysis.test.ts +227 -4
  179. package/src/adapters/mysql/tools/performance/__tests__/optimization.test.ts +35 -0
  180. package/src/adapters/mysql/tools/performance/analysis.ts +75 -21
  181. package/src/adapters/mysql/tools/performance/optimization.ts +44 -6
  182. package/src/adapters/mysql/tools/security/data-protection.ts +10 -4
  183. package/src/adapters/mysql/tools/shell/__tests__/common.test.ts +46 -0
  184. package/src/adapters/mysql/tools/shell/__tests__/restore.test.ts +28 -1
  185. package/src/adapters/mysql/tools/shell/common.ts +34 -2
  186. package/src/adapters/mysql/tools/shell/restore.ts +70 -7
  187. package/src/adapters/mysql/tools/spatial/__tests__/operations.test.ts +29 -0
  188. package/src/adapters/mysql/tools/spatial/operations.ts +13 -2
  189. package/src/adapters/mysql/tools/spatial/setup.ts +23 -0
  190. package/src/adapters/mysql/tools/sysschema/__tests__/resources.test.ts +21 -0
  191. package/src/adapters/mysql/tools/sysschema/resources.ts +5 -0
  192. package/src/adapters/mysql/tools/text/fulltext.ts +13 -5
  193. package/src/adapters/mysql/tools/text/processing.ts +20 -49
  194. package/src/adapters/mysql/tools/transactions.ts +11 -7
  195. package/src/adapters/mysql/types.ts +1241 -87
  196. package/src/auth/scopes.ts +1 -0
  197. package/src/cli/args.ts +14 -0
  198. package/src/codemode/api.ts +1224 -0
  199. package/src/codemode/index.ts +51 -0
  200. package/src/codemode/sandbox-factory.ts +146 -0
  201. package/src/codemode/sandbox.ts +450 -0
  202. package/src/codemode/security.ts +188 -0
  203. package/src/codemode/types.ts +194 -0
  204. package/src/codemode/worker-sandbox.ts +326 -0
  205. package/src/codemode/worker-script.ts +144 -0
  206. package/src/constants/ServerInstructions.ts +33 -9
  207. package/src/filtering/ToolConstants.ts +37 -19
  208. package/src/filtering/ToolFilter.ts +15 -0
  209. package/src/filtering/__tests__/ToolFilter.test.ts +65 -38
  210. package/src/server/McpServer.ts +1 -1
  211. package/src/types/modules/server.ts +3 -0
  212. package/src/types/modules/tools.ts +2 -1
  213. package/src/utils/logger.ts +2 -1
@@ -10,7 +10,13 @@ import {
10
10
  interface CapabilitiesResult {
11
11
  server: {
12
12
  version: string;
13
- features: { json: boolean; fulltext: boolean; partitioning: boolean };
13
+ features: {
14
+ json: boolean;
15
+ fulltext: boolean;
16
+ partitioning: boolean;
17
+ replication: boolean;
18
+ gtid: boolean;
19
+ };
14
20
  };
15
21
  toolGroups: unknown[];
16
22
  metaGroups: unknown[];
@@ -64,6 +70,49 @@ describe("Capabilities Resource", () => {
64
70
  expect(result.server.features).toHaveProperty("partitioning");
65
71
  });
66
72
 
73
+ it("should detect features correctly for MySQL 9.x", async () => {
74
+ mockAdapter.executeQuery.mockResolvedValue(
75
+ createMockQueryResult([{ version: "9.6.0" }]),
76
+ );
77
+
78
+ const resource = createCapabilitiesResource(
79
+ mockAdapter as unknown as MySQLAdapter,
80
+ );
81
+ const result = (await resource.handler(
82
+ "mysql://capabilities",
83
+ mockContext,
84
+ )) as CapabilitiesResult;
85
+
86
+ expect(result.server.version).toBe("9.6.0");
87
+ expect(result.server.features.json).toBe(true);
88
+ expect(result.server.features.gtid).toBe(true);
89
+ expect(result.server.features.fulltext).toBe(true);
90
+ expect(result.server.features.replication).toBe(true);
91
+ expect(result.server.features.partitioning).toBe(true);
92
+ });
93
+
94
+ it("should disable version-dependent features for unknown version", async () => {
95
+ mockAdapter.executeQuery.mockResolvedValue(
96
+ createMockQueryResult([{ version: "unknown" }]),
97
+ );
98
+
99
+ const resource = createCapabilitiesResource(
100
+ mockAdapter as unknown as MySQLAdapter,
101
+ );
102
+ const result = (await resource.handler(
103
+ "mysql://capabilities",
104
+ mockContext,
105
+ )) as CapabilitiesResult;
106
+
107
+ expect(result.server.version).toBe("unknown");
108
+ expect(result.server.features.json).toBe(false);
109
+ expect(result.server.features.gtid).toBe(false);
110
+ // Always-true features remain true
111
+ expect(result.server.features.fulltext).toBe(true);
112
+ expect(result.server.features.replication).toBe(true);
113
+ expect(result.server.features.partitioning).toBe(true);
114
+ });
115
+
67
116
  it("should have correct metadata", () => {
68
117
  const resource = createCapabilitiesResource(
69
118
  mockAdapter as unknown as MySQLAdapter,
@@ -37,16 +37,18 @@ export function createCapabilitiesResource(
37
37
  const version =
38
38
  (versionResult.rows?.[0]?.["version"] as string) ?? "unknown";
39
39
 
40
- // Get available features
40
+ // Parse major version for feature detection (handles 8.x, 9.x, 10.x+)
41
+ const majorVersion = parseInt(version.split(".")[0] ?? "0", 10) || 0;
42
+
41
43
  const features = {
42
- json: version.startsWith("5.7") || version.startsWith("8."),
44
+ json: majorVersion >= 8 || version.startsWith("5.7"),
43
45
  fulltext: true,
44
46
  partitioning: true,
45
47
  replication: true,
46
48
  gtid:
49
+ majorVersion >= 8 ||
47
50
  version.startsWith("5.6") ||
48
- version.startsWith("5.7") ||
49
- version.startsWith("8."),
51
+ version.startsWith("5.7"),
50
52
  };
51
53
 
52
54
  // Get tool groups and meta-groups info
@@ -32,7 +32,7 @@ import { createSpatialResource } from "./spatial.js";
32
32
  import { createDocstoreResource } from "./docstore.js";
33
33
 
34
34
  /**
35
- * Get all MySQL resources (12 total)
35
+ * Get all MySQL resources (18 total)
36
36
  *
37
37
  * Core (6):
38
38
  * - mysql://schema - Full database schema
@@ -49,6 +49,14 @@ import { createDocstoreResource } from "./docstore.js";
49
49
  * - mysql://indexes - Index usage and statistics
50
50
  * - mysql://replication - Replication status and lag
51
51
  * - mysql://innodb - InnoDB buffer pool and engine metrics
52
+ *
53
+ * New (6):
54
+ * - mysql://events - Event Scheduler status and scheduled events
55
+ * - mysql://sysschema - sys schema diagnostics summary
56
+ * - mysql://locks - InnoDB lock contention detection
57
+ * - mysql://cluster - Group Replication/InnoDB Cluster status
58
+ * - mysql://spatial - Spatial columns and indexes
59
+ * - mysql://docstore - Document Store collections
52
60
  */
53
61
  export function getMySQLResources(adapter: MySQLAdapter): ResourceDefinition[] {
54
62
  return [
@@ -180,6 +180,40 @@ describe("Handler Execution", () => {
180
180
  "txn-123",
181
181
  );
182
182
  });
183
+
184
+ it("should return structured error for nonexistent table", async () => {
185
+ mockAdapter.executeReadQuery.mockRejectedValue(
186
+ new Error("Table 'testdb.nonexistent' doesn't exist"),
187
+ );
188
+
189
+ const tool = tools.find((t) => t.name === "mysql_read_query")!;
190
+ const result = await tool.handler(
191
+ { query: "SELECT * FROM nonexistent" },
192
+ mockContext,
193
+ );
194
+
195
+ expect(result).toHaveProperty("success", false);
196
+ expect((result as Record<string, unknown>).error).toContain(
197
+ "doesn't exist",
198
+ );
199
+ });
200
+
201
+ it("should return structured error for non-table errors in read query", async () => {
202
+ mockAdapter.executeReadQuery.mockRejectedValue(
203
+ new Error("Access denied"),
204
+ );
205
+
206
+ const tool = tools.find((t) => t.name === "mysql_read_query")!;
207
+ const result = await tool.handler(
208
+ { query: "SELECT * FROM users" },
209
+ mockContext,
210
+ );
211
+
212
+ expect((result as Record<string, unknown>).success).toBe(false);
213
+ expect((result as Record<string, unknown>).error).toContain(
214
+ "Access denied",
215
+ );
216
+ });
183
217
  });
184
218
 
185
219
  describe("mysql_write_query", () => {
@@ -223,6 +257,40 @@ describe("Handler Execution", () => {
223
257
  "txn-456",
224
258
  );
225
259
  });
260
+
261
+ it("should return structured error for nonexistent table", async () => {
262
+ mockAdapter.executeWriteQuery.mockRejectedValue(
263
+ new Error("Table 'testdb.nonexistent' doesn't exist"),
264
+ );
265
+
266
+ const tool = tools.find((t) => t.name === "mysql_write_query")!;
267
+ const result = await tool.handler(
268
+ { query: "INSERT INTO nonexistent (id) VALUES (1)" },
269
+ mockContext,
270
+ );
271
+
272
+ expect(result).toHaveProperty("success", false);
273
+ expect((result as Record<string, unknown>).error).toContain(
274
+ "doesn't exist",
275
+ );
276
+ });
277
+
278
+ it("should return structured error for non-table errors in write query", async () => {
279
+ mockAdapter.executeWriteQuery.mockRejectedValue(
280
+ new Error("Access denied"),
281
+ );
282
+
283
+ const tool = tools.find((t) => t.name === "mysql_write_query")!;
284
+ const result = await tool.handler(
285
+ { query: "INSERT INTO users VALUES (1)" },
286
+ mockContext,
287
+ );
288
+
289
+ expect((result as Record<string, unknown>).success).toBe(false);
290
+ expect((result as Record<string, unknown>).error).toContain(
291
+ "Access denied",
292
+ );
293
+ });
226
294
  });
227
295
 
228
296
  describe("mysql_list_tables", () => {
@@ -146,6 +146,31 @@ describe("Handler Execution", () => {
146
146
  const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
147
147
  expect(call).toContain("RENAME TO");
148
148
  });
149
+
150
+ it("should place RENAME TO before COMMENT and DO body in combined alter", async () => {
151
+ mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
152
+
153
+ const tool = tools.find((t) => t.name === "mysql_event_alter")!;
154
+ await tool.handler(
155
+ {
156
+ name: "old_event",
157
+ newName: "new_event",
158
+ body: "SELECT 1",
159
+ comment: "updated",
160
+ },
161
+ mockContext,
162
+ );
163
+
164
+ const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
165
+ const renameIndex = call.indexOf("RENAME TO");
166
+ const commentIndex = call.indexOf("COMMENT");
167
+ const doIndex = call.indexOf("DO ");
168
+ expect(renameIndex).toBeGreaterThan(-1);
169
+ expect(commentIndex).toBeGreaterThan(-1);
170
+ expect(doIndex).toBeGreaterThan(-1);
171
+ expect(renameIndex).toBeLessThan(commentIndex);
172
+ expect(commentIndex).toBeLessThan(doIndex);
173
+ });
149
174
  });
150
175
 
151
176
  describe("mysql_event_drop", () => {
@@ -351,7 +376,9 @@ describe("Event Create Advanced", () => {
351
376
  });
352
377
 
353
378
  it("should add IF NOT EXISTS clause when specified", async () => {
354
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
379
+ // Pre-check returns empty (event doesn't exist), then CREATE succeeds
380
+ mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
381
+ mockAdapter.executeQuery.mockResolvedValueOnce(createMockQueryResult([]));
355
382
 
356
383
  const tool = tools.find((t) => t.name === "mysql_event_create")!;
357
384
  await tool.handler(
@@ -364,10 +391,37 @@ describe("Event Create Advanced", () => {
364
391
  mockContext,
365
392
  );
366
393
 
367
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
394
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
368
395
  expect(call).toContain("IF NOT EXISTS");
369
396
  });
370
397
 
398
+ it("should return skipped when ifNotExists is true and event already exists", async () => {
399
+ // Pre-check returns existing event
400
+ mockAdapter.executeQuery.mockResolvedValueOnce(
401
+ createMockQueryResult([{ EVENT_NAME: "my_event" }]),
402
+ );
403
+
404
+ const tool = tools.find((t) => t.name === "mysql_event_create")!;
405
+ const result = await tool.handler(
406
+ {
407
+ name: "my_event",
408
+ schedule: { type: "ONE TIME", executeAt: "2024-12-31 23:59:59" },
409
+ body: "DELETE FROM temp",
410
+ ifNotExists: true,
411
+ },
412
+ mockContext,
413
+ );
414
+
415
+ expect(result).toEqual({
416
+ success: true,
417
+ skipped: true,
418
+ reason: "Event already exists",
419
+ eventName: "my_event",
420
+ });
421
+ // Should only have the pre-check query, no CREATE
422
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(1);
423
+ });
424
+
371
425
  it("should include STARTS and ENDS for recurring events", async () => {
372
426
  mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
373
427
 
@@ -123,7 +123,7 @@ describe("JSON Core Handler Execution", () => {
123
123
  {
124
124
  table: "users",
125
125
  column: "metadata",
126
- candidate: { key: "value" },
126
+ value: { key: "value" },
127
127
  },
128
128
  mockContext,
129
129
  );
@@ -72,19 +72,61 @@ describe("JSON Helper Handler Execution", () => {
72
72
  });
73
73
 
74
74
  describe("mysql_json_validate", () => {
75
- it("should validate JSON value", async () => {
75
+ it("should validate valid JSON", async () => {
76
76
  mockAdapter.executeReadQuery.mockResolvedValue(
77
77
  createMockQueryResult([{ is_valid: 1 }]),
78
78
  );
79
79
 
80
80
  const tool = tools.find((t) => t.name === "mysql_json_validate")!;
81
- const result = await tool.handler(
81
+ const result = (await tool.handler(
82
82
  { value: '{"name":"test"}' },
83
83
  mockContext,
84
- );
84
+ )) as { valid: boolean };
85
85
 
86
86
  expect(mockAdapter.executeReadQuery).toHaveBeenCalled();
87
- expect(result).toBeDefined();
87
+ expect(result.valid).toBe(true);
88
+ });
89
+
90
+ it("should return valid: false for malformed JSON", async () => {
91
+ mockAdapter.executeReadQuery.mockResolvedValue(
92
+ createMockQueryResult([{ is_valid: 0 }]),
93
+ );
94
+
95
+ const tool = tools.find((t) => t.name === "mysql_json_validate")!;
96
+ const result = (await tool.handler(
97
+ { value: '{"broken": true' },
98
+ mockContext,
99
+ )) as { valid: boolean };
100
+
101
+ expect(result.valid).toBe(false);
102
+ });
103
+
104
+ it("should return valid: false for bare strings", async () => {
105
+ mockAdapter.executeReadQuery.mockResolvedValue(
106
+ createMockQueryResult([{ is_valid: 0 }]),
107
+ );
108
+
109
+ const tool = tools.find((t) => t.name === "mysql_json_validate")!;
110
+ const result = (await tool.handler({ value: "hello" }, mockContext)) as {
111
+ valid: boolean;
112
+ };
113
+
114
+ expect(result.valid).toBe(false);
115
+ });
116
+
117
+ it("should return structured error on MySQL failure", async () => {
118
+ mockAdapter.executeReadQuery.mockRejectedValue(
119
+ new Error("Validation query failed"),
120
+ );
121
+
122
+ const tool = tools.find((t) => t.name === "mysql_json_validate")!;
123
+ const result = (await tool.handler(
124
+ { value: "\x00invalid" },
125
+ mockContext,
126
+ )) as { valid: boolean; error: string };
127
+
128
+ expect(result.valid).toBe(false);
129
+ expect(result.error).toBe("Validation query failed");
88
130
  });
89
131
  });
90
132
 
@@ -297,7 +297,9 @@ describe("Partitioning Handler Execution", () => {
297
297
 
298
298
  describe("mysql_add_partition", () => {
299
299
  it("should add a RANGE partition", async () => {
300
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
300
+ mockAdapter.executeQuery
301
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
302
+ .mockResolvedValueOnce(createMockQueryResult([]));
301
303
 
302
304
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
303
305
  const result = await tool.handler(
@@ -310,15 +312,19 @@ describe("Partitioning Handler Execution", () => {
310
312
  mockContext,
311
313
  );
312
314
 
313
- expect(mockAdapter.executeQuery).toHaveBeenCalled();
314
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
315
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
316
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
315
317
  expect(call).toContain("ADD PARTITION");
316
318
  expect(call).toContain("VALUES LESS THAN");
317
319
  expect(result).toHaveProperty("success", true);
318
320
  });
319
321
 
320
322
  it("should add a LIST partition", async () => {
321
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
323
+ mockAdapter.executeQuery
324
+ .mockResolvedValueOnce(
325
+ createMockQueryResult([{ TABLE_NAME: "regions" }]),
326
+ )
327
+ .mockResolvedValueOnce(createMockQueryResult([]));
322
328
 
323
329
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
324
330
  await tool.handler(
@@ -331,12 +337,14 @@ describe("Partitioning Handler Execution", () => {
331
337
  mockContext,
332
338
  );
333
339
 
334
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
340
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
335
341
  expect(call).toContain("VALUES IN");
336
342
  });
337
343
 
338
344
  it("should add HASH partitions", async () => {
339
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
345
+ mockAdapter.executeQuery
346
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "data" }]))
347
+ .mockResolvedValueOnce(createMockQueryResult([]));
340
348
 
341
349
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
342
350
  await tool.handler(
@@ -349,12 +357,14 @@ describe("Partitioning Handler Execution", () => {
349
357
  mockContext,
350
358
  );
351
359
 
352
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
360
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
353
361
  expect(call).toContain("PARTITIONS 4");
354
362
  });
355
363
 
356
364
  it("should add KEY partitions", async () => {
357
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
365
+ mockAdapter.executeQuery
366
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "data" }]))
367
+ .mockResolvedValueOnce(createMockQueryResult([]));
358
368
 
359
369
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
360
370
  await tool.handler(
@@ -367,14 +377,16 @@ describe("Partitioning Handler Execution", () => {
367
377
  mockContext,
368
378
  );
369
379
 
370
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
380
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
371
381
  expect(call).toContain("PARTITIONS 8");
372
382
  });
373
383
  });
374
384
 
375
385
  describe("mysql_drop_partition", () => {
376
386
  it("should drop a partition", async () => {
377
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
387
+ mockAdapter.executeQuery
388
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
389
+ .mockResolvedValueOnce(createMockQueryResult([]));
378
390
 
379
391
  const tool = tools.find((t) => t.name === "mysql_drop_partition")!;
380
392
  const result = await tool.handler(
@@ -385,8 +397,8 @@ describe("Partitioning Handler Execution", () => {
385
397
  mockContext,
386
398
  );
387
399
 
388
- expect(mockAdapter.executeQuery).toHaveBeenCalled();
389
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
400
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
401
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
390
402
  expect(call).toContain("DROP PARTITION");
391
403
  expect(result).toHaveProperty("success", true);
392
404
  });
@@ -394,7 +406,9 @@ describe("Partitioning Handler Execution", () => {
394
406
 
395
407
  describe("mysql_reorganize_partition", () => {
396
408
  it("should reorganize partitions", async () => {
397
- mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
409
+ mockAdapter.executeQuery
410
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
411
+ .mockResolvedValueOnce(createMockQueryResult([]));
398
412
 
399
413
  const tool = tools.find((t) => t.name === "mysql_reorganize_partition")!;
400
414
  const result = await tool.handler(
@@ -410,18 +424,20 @@ describe("Partitioning Handler Execution", () => {
410
424
  mockContext,
411
425
  );
412
426
 
413
- expect(mockAdapter.executeQuery).toHaveBeenCalled();
414
- const call = mockAdapter.executeQuery.mock.calls[0][0] as string;
427
+ expect(mockAdapter.executeQuery).toHaveBeenCalledTimes(2);
428
+ const call = mockAdapter.executeQuery.mock.calls[1][0] as string;
415
429
  expect(call).toContain("REORGANIZE PARTITION");
416
430
  expect(result).toHaveProperty("success", true);
417
431
  });
418
432
 
419
433
  it("should return structured error for non-partitioned table", async () => {
420
- mockAdapter.executeQuery.mockRejectedValue(
421
- new Error(
422
- "Partition management on a not partitioned table is not possible",
423
- ),
424
- );
434
+ mockAdapter.executeQuery
435
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "users" }]))
436
+ .mockRejectedValueOnce(
437
+ new Error(
438
+ "Partition management on a not partitioned table is not possible",
439
+ ),
440
+ );
425
441
 
426
442
  const tool = tools.find((t) => t.name === "mysql_reorganize_partition")!;
427
443
  const result = (await tool.handler(
@@ -439,9 +455,11 @@ describe("Partitioning Handler Execution", () => {
439
455
  });
440
456
 
441
457
  it("should return structured error for nonexistent partition", async () => {
442
- mockAdapter.executeQuery.mockRejectedValue(
443
- new Error("Error in list of partitions to REORGANIZE"),
444
- );
458
+ mockAdapter.executeQuery
459
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
460
+ .mockRejectedValueOnce(
461
+ new Error("Error in list of partitions to REORGANIZE"),
462
+ );
445
463
 
446
464
  const tool = tools.find((t) => t.name === "mysql_reorganize_partition")!;
447
465
  const result = (await tool.handler(
@@ -458,6 +476,76 @@ describe("Partitioning Handler Execution", () => {
458
476
  expect(result.fromPartitions).toEqual(["nonexistent"]);
459
477
  expect(result.error).toContain("do not exist");
460
478
  });
479
+
480
+ it("should return structured error for unsupported partition type (HASH)", async () => {
481
+ const tool = tools.find((t) => t.name === "mysql_reorganize_partition")!;
482
+ const result = (await tool.handler(
483
+ {
484
+ table: "data",
485
+ fromPartitions: ["p1"],
486
+ partitionType: "HASH",
487
+ toPartitions: [{ name: "p1a", value: "50" }],
488
+ },
489
+ mockContext,
490
+ )) as { success: boolean; error: string };
491
+
492
+ expect(result.success).toBe(false);
493
+ expect(result.error).toContain("HASH/KEY");
494
+ expect(result.error).toContain("cannot be reorganized");
495
+ // Should NOT have called executeQuery — error caught at validation
496
+ expect(mockAdapter.executeQuery).not.toHaveBeenCalled();
497
+ });
498
+ });
499
+
500
+ describe("Partitioning P154 existence checks", () => {
501
+ it("should return exists: false for nonexistent table in add_partition", async () => {
502
+ mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
503
+
504
+ const tool = tools.find((t) => t.name === "mysql_add_partition")!;
505
+ const result = (await tool.handler(
506
+ {
507
+ table: "nonexistent",
508
+ partitionName: "p1",
509
+ partitionType: "RANGE",
510
+ value: "100",
511
+ },
512
+ mockContext,
513
+ )) as { exists: boolean; table: string };
514
+
515
+ expect(result.exists).toBe(false);
516
+ expect(result.table).toBe("nonexistent");
517
+ });
518
+
519
+ it("should return exists: false for nonexistent table in drop_partition", async () => {
520
+ mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
521
+
522
+ const tool = tools.find((t) => t.name === "mysql_drop_partition")!;
523
+ const result = (await tool.handler(
524
+ { table: "nonexistent", partitionName: "p1" },
525
+ mockContext,
526
+ )) as { exists: boolean; table: string };
527
+
528
+ expect(result.exists).toBe(false);
529
+ expect(result.table).toBe("nonexistent");
530
+ });
531
+
532
+ it("should return exists: false for nonexistent table in reorganize_partition", async () => {
533
+ mockAdapter.executeQuery.mockResolvedValue(createMockQueryResult([]));
534
+
535
+ const tool = tools.find((t) => t.name === "mysql_reorganize_partition")!;
536
+ const result = (await tool.handler(
537
+ {
538
+ table: "nonexistent",
539
+ fromPartitions: ["p1"],
540
+ partitionType: "RANGE",
541
+ toPartitions: [{ name: "p1a", value: "50" }],
542
+ },
543
+ mockContext,
544
+ )) as { exists: boolean; table: string };
545
+
546
+ expect(result.exists).toBe(false);
547
+ expect(result.table).toBe("nonexistent");
548
+ });
461
549
  });
462
550
 
463
551
  describe("mysql_partition_info existence check", () => {
@@ -477,11 +565,13 @@ describe("Partitioning Handler Execution", () => {
477
565
 
478
566
  describe("mysql_add_partition error handling", () => {
479
567
  it("should return structured error for non-partitioned table", async () => {
480
- mockAdapter.executeQuery.mockRejectedValue(
481
- new Error(
482
- "Partition management on a not partitioned table is not possible",
483
- ),
484
- );
568
+ mockAdapter.executeQuery
569
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "users" }]))
570
+ .mockRejectedValueOnce(
571
+ new Error(
572
+ "Partition management on a not partitioned table is not possible",
573
+ ),
574
+ );
485
575
 
486
576
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
487
577
  const result = (await tool.handler(
@@ -499,9 +589,11 @@ describe("Partitioning Handler Execution", () => {
499
589
  });
500
590
 
501
591
  it("should return structured error for MAXVALUE conflict", async () => {
502
- mockAdapter.executeQuery.mockRejectedValue(
503
- new Error("MAXVALUE can only be used in last partition definition"),
504
- );
592
+ mockAdapter.executeQuery
593
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
594
+ .mockRejectedValueOnce(
595
+ new Error("MAXVALUE can only be used in last partition definition"),
596
+ );
505
597
 
506
598
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
507
599
  const result = (await tool.handler(
@@ -520,9 +612,15 @@ describe("Partitioning Handler Execution", () => {
520
612
  });
521
613
 
522
614
  it("should return structured error for duplicate partition values", async () => {
523
- mockAdapter.executeQuery.mockRejectedValue(
524
- new Error("Multiple definition of same constant in list partitioning"),
525
- );
615
+ mockAdapter.executeQuery
616
+ .mockResolvedValueOnce(
617
+ createMockQueryResult([{ TABLE_NAME: "regions" }]),
618
+ )
619
+ .mockRejectedValueOnce(
620
+ new Error(
621
+ "Multiple definition of same constant in list partitioning",
622
+ ),
623
+ );
526
624
 
527
625
  const tool = tools.find((t) => t.name === "mysql_add_partition")!;
528
626
  const result = (await tool.handler(
@@ -542,11 +640,13 @@ describe("Partitioning Handler Execution", () => {
542
640
 
543
641
  describe("mysql_drop_partition error handling", () => {
544
642
  it("should return structured error for non-partitioned table", async () => {
545
- mockAdapter.executeQuery.mockRejectedValue(
546
- new Error(
547
- "Partition management on a not partitioned table is not possible",
548
- ),
549
- );
643
+ mockAdapter.executeQuery
644
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "users" }]))
645
+ .mockRejectedValueOnce(
646
+ new Error(
647
+ "Partition management on a not partitioned table is not possible",
648
+ ),
649
+ );
550
650
 
551
651
  const tool = tools.find((t) => t.name === "mysql_drop_partition")!;
552
652
  const result = (await tool.handler(
@@ -559,9 +659,11 @@ describe("Partitioning Handler Execution", () => {
559
659
  });
560
660
 
561
661
  it("should return structured error for nonexistent partition", async () => {
562
- mockAdapter.executeQuery.mockRejectedValue(
563
- new Error("Error in list of partitions to DROP"),
564
- );
662
+ mockAdapter.executeQuery
663
+ .mockResolvedValueOnce(createMockQueryResult([{ TABLE_NAME: "logs" }]))
664
+ .mockRejectedValueOnce(
665
+ new Error("Error in list of partitions to DROP"),
666
+ );
565
667
 
566
668
  const tool = tools.find((t) => t.name === "mysql_drop_partition")!;
567
669
  const result = (await tool.handler(