@neverinfamous/postgres-mcp 1.2.0 → 2.0.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 (293) hide show
  1. package/README.md +202 -148
  2. package/dist/__tests__/benchmarks/codemode.bench.d.ts +10 -0
  3. package/dist/__tests__/benchmarks/codemode.bench.d.ts.map +1 -0
  4. package/dist/__tests__/benchmarks/codemode.bench.js +159 -0
  5. package/dist/__tests__/benchmarks/codemode.bench.js.map +1 -0
  6. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts +10 -0
  7. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts.map +1 -0
  8. package/dist/__tests__/benchmarks/connection-pool.bench.js +123 -0
  9. package/dist/__tests__/benchmarks/connection-pool.bench.js.map +1 -0
  10. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts +11 -0
  11. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts.map +1 -0
  12. package/dist/__tests__/benchmarks/handler-dispatch.bench.js +199 -0
  13. package/dist/__tests__/benchmarks/handler-dispatch.bench.js.map +1 -0
  14. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts +15 -0
  15. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts.map +1 -0
  16. package/dist/__tests__/benchmarks/logger-sanitization.bench.js +155 -0
  17. package/dist/__tests__/benchmarks/logger-sanitization.bench.js.map +1 -0
  18. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts +10 -0
  19. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts.map +1 -0
  20. package/dist/__tests__/benchmarks/resource-prompts.bench.js +181 -0
  21. package/dist/__tests__/benchmarks/resource-prompts.bench.js.map +1 -0
  22. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts +11 -0
  23. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts.map +1 -0
  24. package/dist/__tests__/benchmarks/schema-parsing.bench.js +209 -0
  25. package/dist/__tests__/benchmarks/schema-parsing.bench.js.map +1 -0
  26. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts +9 -0
  27. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts.map +1 -0
  28. package/dist/__tests__/benchmarks/tool-filtering.bench.js +83 -0
  29. package/dist/__tests__/benchmarks/tool-filtering.bench.js.map +1 -0
  30. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts +10 -0
  31. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts.map +1 -0
  32. package/dist/__tests__/benchmarks/transport-auth.bench.js +128 -0
  33. package/dist/__tests__/benchmarks/transport-auth.bench.js.map +1 -0
  34. package/dist/__tests__/benchmarks/utilities.bench.d.ts +10 -0
  35. package/dist/__tests__/benchmarks/utilities.bench.d.ts.map +1 -0
  36. package/dist/__tests__/benchmarks/utilities.bench.js +164 -0
  37. package/dist/__tests__/benchmarks/utilities.bench.js.map +1 -0
  38. package/dist/adapters/DatabaseAdapter.d.ts.map +1 -1
  39. package/dist/adapters/DatabaseAdapter.js +12 -0
  40. package/dist/adapters/DatabaseAdapter.js.map +1 -1
  41. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  42. package/dist/adapters/postgresql/PostgresAdapter.js +56 -3
  43. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  44. package/dist/adapters/postgresql/prompts/ltree.js +2 -2
  45. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  46. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  47. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  48. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  49. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  50. package/dist/adapters/postgresql/schemas/backup.d.ts +45 -27
  51. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  52. package/dist/adapters/postgresql/schemas/backup.js +64 -26
  53. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  54. package/dist/adapters/postgresql/schemas/core.d.ts +53 -19
  55. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  56. package/dist/adapters/postgresql/schemas/core.js +61 -17
  57. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  58. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  59. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  60. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  61. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  62. package/dist/adapters/postgresql/schemas/extensions.d.ts +224 -110
  63. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  64. package/dist/adapters/postgresql/schemas/extensions.js +245 -96
  65. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  66. package/dist/adapters/postgresql/schemas/index.d.ts +7 -6
  67. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  68. package/dist/adapters/postgresql/schemas/index.js +16 -8
  69. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  70. package/dist/adapters/postgresql/schemas/introspection.d.ts +445 -0
  71. package/dist/adapters/postgresql/schemas/introspection.d.ts.map +1 -0
  72. package/dist/adapters/postgresql/schemas/introspection.js +478 -0
  73. package/dist/adapters/postgresql/schemas/introspection.js.map +1 -0
  74. package/dist/adapters/postgresql/schemas/jsonb.d.ts +102 -42
  75. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  76. package/dist/adapters/postgresql/schemas/jsonb.js +125 -30
  77. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  78. package/dist/adapters/postgresql/schemas/monitoring.d.ts +69 -36
  79. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  80. package/dist/adapters/postgresql/schemas/monitoring.js +98 -40
  81. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  82. package/dist/adapters/postgresql/schemas/partitioning.d.ts +21 -24
  83. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  84. package/dist/adapters/postgresql/schemas/partitioning.js +26 -14
  85. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  86. package/dist/adapters/postgresql/schemas/partman.d.ts +69 -0
  87. package/dist/adapters/postgresql/schemas/partman.d.ts.map +1 -1
  88. package/dist/adapters/postgresql/schemas/partman.js +46 -33
  89. package/dist/adapters/postgresql/schemas/partman.js.map +1 -1
  90. package/dist/adapters/postgresql/schemas/performance.d.ts +97 -49
  91. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  92. package/dist/adapters/postgresql/schemas/performance.js +139 -34
  93. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  94. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  95. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  96. package/dist/adapters/postgresql/schemas/postgis.js +40 -0
  97. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  98. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +50 -30
  99. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  100. package/dist/adapters/postgresql/schemas/schema-mgmt.js +105 -33
  101. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  102. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  103. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  104. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  105. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  106. package/dist/adapters/postgresql/schemas/text-search.d.ts +34 -19
  107. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  108. package/dist/adapters/postgresql/schemas/text-search.js +52 -13
  109. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  110. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  111. package/dist/adapters/postgresql/tools/admin.js +272 -186
  112. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  113. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  114. package/dist/adapters/postgresql/tools/backup/dump.js +376 -350
  115. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  116. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  117. package/dist/adapters/postgresql/tools/citext.js +333 -243
  118. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  119. package/dist/adapters/postgresql/tools/codemode/index.d.ts.map +1 -1
  120. package/dist/adapters/postgresql/tools/codemode/index.js +2 -11
  121. package/dist/adapters/postgresql/tools/codemode/index.js.map +1 -1
  122. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  123. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  124. package/dist/adapters/postgresql/tools/core/convenience.js +101 -19
  125. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  126. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  127. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  128. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  129. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  130. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  131. package/dist/adapters/postgresql/tools/core/health.js +18 -4
  132. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  133. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  134. package/dist/adapters/postgresql/tools/core/indexes.js +48 -6
  135. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  136. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  137. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  138. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  139. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  140. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  141. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  142. package/dist/adapters/postgresql/tools/core/schemas.d.ts +51 -25
  143. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  144. package/dist/adapters/postgresql/tools/core/schemas.js +51 -25
  145. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  146. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  147. package/dist/adapters/postgresql/tools/core/tables.js +72 -32
  148. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  149. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  150. package/dist/adapters/postgresql/tools/cron.js +333 -206
  151. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  152. package/dist/adapters/postgresql/tools/introspection.d.ts +15 -0
  153. package/dist/adapters/postgresql/tools/introspection.d.ts.map +1 -0
  154. package/dist/adapters/postgresql/tools/introspection.js +1682 -0
  155. package/dist/adapters/postgresql/tools/introspection.js.map +1 -0
  156. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  157. package/dist/adapters/postgresql/tools/jsonb/advanced.js +394 -297
  158. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  159. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  160. package/dist/adapters/postgresql/tools/jsonb/basic.js +686 -398
  161. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  162. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  163. package/dist/adapters/postgresql/tools/kcache.js +278 -246
  164. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  165. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  166. package/dist/adapters/postgresql/tools/ltree.js +137 -38
  167. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  168. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  169. package/dist/adapters/postgresql/tools/monitoring.js +86 -55
  170. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  171. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  172. package/dist/adapters/postgresql/tools/partitioning.js +79 -15
  173. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  174. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  175. package/dist/adapters/postgresql/tools/partman/management.js +43 -56
  176. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  177. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  178. package/dist/adapters/postgresql/tools/partman/operations.js +137 -24
  179. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  180. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  181. package/dist/adapters/postgresql/tools/performance/analysis.js +276 -165
  182. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  183. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  184. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  185. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  186. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  187. package/dist/adapters/postgresql/tools/performance/monitoring.js +52 -12
  188. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  189. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  190. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  191. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  192. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  193. package/dist/adapters/postgresql/tools/performance/stats.js +182 -60
  194. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  195. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  196. package/dist/adapters/postgresql/tools/pgcrypto.js +277 -102
  197. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  198. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  199. package/dist/adapters/postgresql/tools/postgis/advanced.js +298 -230
  200. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  201. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  202. package/dist/adapters/postgresql/tools/postgis/basic.js +370 -251
  203. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  204. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  205. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  206. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  207. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  208. package/dist/adapters/postgresql/tools/schema.js +580 -233
  209. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  210. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  211. package/dist/adapters/postgresql/tools/stats/advanced.js +567 -506
  212. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  213. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  214. package/dist/adapters/postgresql/tools/stats/basic.js +340 -316
  215. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  216. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  217. package/dist/adapters/postgresql/tools/text.js +690 -337
  218. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  219. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  220. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  221. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  222. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  223. package/dist/adapters/postgresql/tools/vector/advanced.js +18 -0
  224. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  225. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  226. package/dist/adapters/postgresql/tools/vector/basic.js +100 -53
  227. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  228. package/dist/auth/auth-context.d.ts +28 -0
  229. package/dist/auth/auth-context.d.ts.map +1 -0
  230. package/dist/auth/auth-context.js +37 -0
  231. package/dist/auth/auth-context.js.map +1 -0
  232. package/dist/auth/scope-map.d.ts +20 -0
  233. package/dist/auth/scope-map.d.ts.map +1 -0
  234. package/dist/auth/scope-map.js +40 -0
  235. package/dist/auth/scope-map.js.map +1 -0
  236. package/dist/auth/scopes.d.ts.map +1 -1
  237. package/dist/auth/scopes.js +2 -0
  238. package/dist/auth/scopes.js.map +1 -1
  239. package/dist/cli.js +1 -1
  240. package/dist/cli.js.map +1 -1
  241. package/dist/codemode/api.d.ts +1 -0
  242. package/dist/codemode/api.d.ts.map +1 -1
  243. package/dist/codemode/api.js +35 -1
  244. package/dist/codemode/api.js.map +1 -1
  245. package/dist/codemode/index.d.ts +0 -2
  246. package/dist/codemode/index.d.ts.map +1 -1
  247. package/dist/codemode/index.js +0 -4
  248. package/dist/codemode/index.js.map +1 -1
  249. package/dist/codemode/sandbox.d.ts +14 -1
  250. package/dist/codemode/sandbox.d.ts.map +1 -1
  251. package/dist/codemode/sandbox.js +58 -19
  252. package/dist/codemode/sandbox.js.map +1 -1
  253. package/dist/codemode/types.d.ts.map +1 -1
  254. package/dist/codemode/types.js +3 -0
  255. package/dist/codemode/types.js.map +1 -1
  256. package/dist/constants/ServerInstructions.d.ts +5 -1
  257. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  258. package/dist/constants/ServerInstructions.js +117 -31
  259. package/dist/constants/ServerInstructions.js.map +1 -1
  260. package/dist/filtering/ToolConstants.d.ts +22 -19
  261. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  262. package/dist/filtering/ToolConstants.js +48 -37
  263. package/dist/filtering/ToolConstants.js.map +1 -1
  264. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  265. package/dist/filtering/ToolFilter.js +10 -13
  266. package/dist/filtering/ToolFilter.js.map +1 -1
  267. package/dist/pool/ConnectionPool.js +1 -1
  268. package/dist/pool/ConnectionPool.js.map +1 -1
  269. package/dist/transports/http.d.ts +1 -0
  270. package/dist/transports/http.d.ts.map +1 -1
  271. package/dist/transports/http.js +75 -21
  272. package/dist/transports/http.js.map +1 -1
  273. package/dist/types/filtering.d.ts +2 -2
  274. package/dist/types/filtering.d.ts.map +1 -1
  275. package/dist/utils/icons.d.ts.map +1 -1
  276. package/dist/utils/icons.js +5 -0
  277. package/dist/utils/icons.js.map +1 -1
  278. package/dist/utils/where-clause.d.ts.map +1 -1
  279. package/dist/utils/where-clause.js +24 -0
  280. package/dist/utils/where-clause.js.map +1 -1
  281. package/package.json +20 -13
  282. package/dist/codemode/sandbox-factory.d.ts +0 -72
  283. package/dist/codemode/sandbox-factory.d.ts.map +0 -1
  284. package/dist/codemode/sandbox-factory.js +0 -88
  285. package/dist/codemode/sandbox-factory.js.map +0 -1
  286. package/dist/codemode/worker-sandbox.d.ts +0 -82
  287. package/dist/codemode/worker-sandbox.d.ts.map +0 -1
  288. package/dist/codemode/worker-sandbox.js +0 -244
  289. package/dist/codemode/worker-sandbox.js.map +0 -1
  290. package/dist/codemode/worker-script.d.ts +0 -8
  291. package/dist/codemode/worker-script.d.ts.map +0 -1
  292. package/dist/codemode/worker-script.js +0 -113
  293. package/dist/codemode/worker-script.js.map +0 -1
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Core JSONB operations including extract, set, insert, delete, contains, path query, aggregation, and type checks.
5
5
  */
6
- import { z } from "zod";
6
+ import { z, ZodError } from "zod";
7
7
  import { readOnly, write } from "../../../../utils/annotations.js";
8
8
  import { getToolIcons } from "../../../../utils/icons.js";
9
+ import { formatPostgresError } from "../core/error-helpers.js";
10
+ import { sanitizeWhereClause } from "../../../../utils/where-clause.js";
11
+ import { sanitizeTableName, sanitizeIdentifier, } from "../../../../utils/identifiers.js";
9
12
  import {
10
13
  // Base schemas (for MCP inputSchema visibility)
11
14
  JsonbExtractSchemaBase, JsonbSetSchemaBase, JsonbContainsSchemaBase, JsonbPathQuerySchemaBase, JsonbInsertSchemaBase, JsonbDeleteSchemaBase, JsonbTypeofSchemaBase, JsonbKeysSchemaBase, JsonbStripNullsSchemaBase, JsonbAggSchemaBase,
@@ -22,6 +25,28 @@ JsonbExtractOutputSchema, JsonbSetOutputSchema, JsonbInsertOutputSchema, JsonbDe
22
25
  function toJsonString(value) {
23
26
  return JSON.stringify(value);
24
27
  }
28
+ /**
29
+ * Resolve table and schema for JSONB tools.
30
+ * Validates schema existence when non-public, returns schema-qualified table name.
31
+ * Returns [qualifiedTable, null] on success, or [null, errorResponse] on failure.
32
+ */
33
+ async function resolveJsonbTable(adapter, table, schema) {
34
+ const schemaName = schema ?? "public";
35
+ // Validate schema existence for non-public schemas
36
+ if (schemaName !== "public") {
37
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
38
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
39
+ return [
40
+ null,
41
+ {
42
+ success: false,
43
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
44
+ },
45
+ ];
46
+ }
47
+ }
48
+ return [sanitizeTableName(table, schemaName), null];
49
+ }
25
50
  export function createJsonbExtractTool(adapter) {
26
51
  return {
27
52
  name: "pg_jsonb_extract",
@@ -32,50 +57,72 @@ export function createJsonbExtractTool(adapter) {
32
57
  annotations: readOnly("JSONB Extract"),
33
58
  icons: getToolIcons("jsonb", readOnly("JSONB Extract")),
34
59
  handler: async (params, _context) => {
35
- const parsed = JsonbExtractSchema.parse(params);
36
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
37
- const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
38
- // Use normalizePathToArray for PostgreSQL #> operator
39
- const pathArray = normalizePathToArray(parsed.path);
40
- // After preprocess and refine, table and column are guaranteed set
41
- const table = parsed.table ?? parsed.tableName;
42
- const column = parsed.column ?? parsed.col;
43
- if (!table || !column) {
44
- throw new Error("table and column are required");
45
- }
46
- // Build select expression with optional additional columns
47
- let selectExpr = `"${column}" #> $1 as extracted_value`;
48
- if (parsed.select !== undefined && parsed.select.length > 0) {
49
- const additionalCols = parsed.select
50
- .map((c) => {
51
- // Handle expressions vs simple column names
52
- const needsQuote = !c.includes("->") &&
53
- !c.includes("(") &&
54
- !c.includes("::") &&
55
- /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(c);
56
- return needsQuote ? `"${c}"` : c;
57
- })
58
- .join(", ");
59
- selectExpr = `${additionalCols}, ${selectExpr}`;
60
- }
61
- const sql = `SELECT ${selectExpr} FROM "${table}"${whereClause}${limitClause}`;
62
- const result = await adapter.executeQuery(sql, [pathArray]);
63
- // If select columns were provided, return full row objects
64
- if (parsed.select !== undefined && parsed.select.length > 0) {
65
- const rows = result.rows?.map((r) => {
66
- // Rename extracted_value back to 'value' for consistency
67
- const row = {};
68
- for (const [key, val] of Object.entries(r)) {
69
- if (key === "extracted_value") {
70
- row["value"] = val;
71
- }
72
- else {
73
- row[key] = val;
60
+ try {
61
+ const parsed = JsonbExtractSchema.parse(params);
62
+ const whereClause = parsed.where
63
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
64
+ : "";
65
+ const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
66
+ // Use normalizePathToArray for PostgreSQL #> operator
67
+ const pathArray = normalizePathToArray(parsed.path);
68
+ // After preprocess and refine, table and column are guaranteed set
69
+ const table = parsed.table ?? parsed.tableName;
70
+ const column = parsed.column ?? parsed.col;
71
+ if (!table || !column) {
72
+ return { success: false, error: "table and column are required" };
73
+ }
74
+ // Validate schema and build qualified table name
75
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
76
+ if (tableError)
77
+ return tableError;
78
+ // Build select expression with optional additional columns
79
+ let selectExpr = `${sanitizeIdentifier(column)} #> $1 as extracted_value`;
80
+ if (parsed.select !== undefined && parsed.select.length > 0) {
81
+ const additionalCols = parsed.select
82
+ .map((c) => {
83
+ // Handle expressions vs simple column names
84
+ const needsQuote = !c.includes("->") &&
85
+ !c.includes("(") &&
86
+ !c.includes("::") &&
87
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(c);
88
+ return needsQuote ? `"${c}"` : c;
89
+ })
90
+ .join(", ");
91
+ selectExpr = `${additionalCols}, ${selectExpr}`;
92
+ }
93
+ const sql = `SELECT ${selectExpr} FROM ${qualifiedTable}${whereClause}${limitClause}`;
94
+ const result = await adapter.executeQuery(sql, [pathArray]);
95
+ // If select columns were provided, return full row objects
96
+ if (parsed.select !== undefined && parsed.select.length > 0) {
97
+ const rows = result.rows?.map((r) => {
98
+ // Rename extracted_value back to 'value' for consistency
99
+ const row = {};
100
+ for (const [key, val] of Object.entries(r)) {
101
+ if (key === "extracted_value") {
102
+ row["value"] = val;
103
+ }
104
+ else {
105
+ row[key] = val;
106
+ }
74
107
  }
108
+ return row;
109
+ });
110
+ const allNulls = rows?.every((r) => r["value"] === null) ?? false;
111
+ const response = {
112
+ rows,
113
+ count: rows?.length ?? 0,
114
+ };
115
+ if (allNulls && (rows?.length ?? 0) > 0) {
116
+ response.hint =
117
+ "All values are null - path may not exist in data. Use pg_jsonb_typeof to check.";
75
118
  }
76
- return row;
77
- });
78
- const allNulls = rows?.every((r) => r["value"] === null) ?? false;
119
+ return response;
120
+ }
121
+ // Original behavior: return just the extracted values
122
+ // Wrap each value in an object with 'value' key for consistency with select mode
123
+ const rows = result.rows?.map((r) => ({ value: r["extracted_value"] }));
124
+ // Check if all results are null (path may not exist)
125
+ const allNulls = rows?.every((r) => r.value === null) ?? false;
79
126
  const response = {
80
127
  rows,
81
128
  count: rows?.length ?? 0,
@@ -86,20 +133,14 @@ export function createJsonbExtractTool(adapter) {
86
133
  }
87
134
  return response;
88
135
  }
89
- // Original behavior: return just the extracted values
90
- // Wrap each value in an object with 'value' key for consistency with select mode
91
- const rows = result.rows?.map((r) => ({ value: r["extracted_value"] }));
92
- // Check if all results are null (path may not exist)
93
- const allNulls = rows?.every((r) => r.value === null) ?? false;
94
- const response = {
95
- rows,
96
- count: rows?.length ?? 0,
97
- };
98
- if (allNulls && (rows?.length ?? 0) > 0) {
99
- response.hint =
100
- "All values are null - path may not exist in data. Use pg_jsonb_typeof to check.";
136
+ catch (error) {
137
+ return {
138
+ success: false,
139
+ error: formatPostgresError(error, {
140
+ tool: "pg_jsonb_extract",
141
+ }),
142
+ };
101
143
  }
102
- return response;
103
144
  },
104
145
  };
105
146
  }
@@ -113,72 +154,92 @@ export function createJsonbSetTool(adapter) {
113
154
  annotations: write("JSONB Set"),
114
155
  icons: getToolIcons("jsonb", write("JSONB Set")),
115
156
  handler: async (params, _context) => {
116
- const parsed = JsonbSetSchema.parse(params);
117
- // Resolve table/column from optional aliases
118
- const table = parsed.table ?? parsed.tableName;
119
- const column = parsed.column ?? parsed.col;
120
- if (!table || !column) {
121
- throw new Error("table and column are required");
122
- }
123
- const { value, where, createMissing } = parsed;
124
- // Normalize path to array format
125
- const path = normalizePathToArray(parsed.path);
126
- // Validate required 'where' parameter
127
- if (!where || where.trim() === "") {
128
- throw new Error('pg_jsonb_set requires a WHERE clause to identify rows to update. Example: where: "id = 1"');
129
- }
130
- // Validate value is provided (undefined would set column to null)
131
- if (value === undefined) {
132
- throw new Error("pg_jsonb_set requires a value parameter. To remove a key, use pg_jsonb_delete instead.");
133
- }
134
- const createFlag = createMissing !== false;
135
- // Handle empty path - replace entire column value
136
- if (path.length === 0) {
137
- const sql = `UPDATE "${table}" SET "${column}" = $1::jsonb WHERE ${where}`;
138
- const result = await adapter.executeQuery(sql, [toJsonString(value)]);
139
- return {
140
- rowsAffected: result.rowsAffected,
141
- hint: "Replaced entire column value (empty path)",
142
- };
157
+ try {
158
+ const parsed = JsonbSetSchema.parse(params);
159
+ // Resolve table/column from optional aliases
160
+ const table = parsed.table ?? parsed.tableName;
161
+ const column = parsed.column ?? parsed.col;
162
+ if (!table || !column) {
163
+ return { success: false, error: "table and column are required" };
164
+ }
165
+ const { value, where, createMissing } = parsed;
166
+ // Validate schema and build qualified table name
167
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
168
+ if (tableError)
169
+ return tableError;
170
+ // Normalize path to array format
171
+ const path = normalizePathToArray(parsed.path);
172
+ // Validate required 'where' parameter
173
+ if (!where || where.trim() === "") {
174
+ return {
175
+ success: false,
176
+ error: 'pg_jsonb_set requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
177
+ };
178
+ }
179
+ // Validate value is provided (undefined would set column to null)
180
+ if (value === undefined) {
181
+ return {
182
+ success: false,
183
+ error: "pg_jsonb_set requires a value parameter. To remove a key, use pg_jsonb_delete instead.",
184
+ };
185
+ }
186
+ const createFlag = createMissing !== false;
187
+ // Handle empty path - replace entire column value
188
+ if (path.length === 0) {
189
+ const sql = `UPDATE ${qualifiedTable} SET "${column}" = $1::jsonb WHERE ${sanitizeWhereClause(where)}`;
190
+ const result = await adapter.executeQuery(sql, [toJsonString(value)]);
191
+ return {
192
+ rowsAffected: result.rowsAffected,
193
+ hint: "Replaced entire column value (empty path)",
194
+ };
195
+ }
196
+ // For deep nested paths with createMissing=true, build intermediate objects
197
+ // PostgreSQL's jsonb_set only creates one level, so we nest calls for deep paths
198
+ let sql;
199
+ if (createFlag && path.length > 1) {
200
+ // Build nested jsonb_set calls to ensure each intermediate path exists
201
+ // Start with COALESCE to handle NULL columns
202
+ let expr = `COALESCE("${column}", '{}'::jsonb)`;
203
+ // For each intermediate level, wrap in jsonb_set to initialize to {}
204
+ for (let i = 0; i < path.length - 1; i++) {
205
+ const subPath = path.slice(0, i + 1);
206
+ const pathStr = "{" + subPath.join(",") + "}";
207
+ // Use COALESCE on the extraction from current expr, not original column
208
+ // This properly chains the nested creation
209
+ expr = `jsonb_set(${expr}, '${pathStr}'::text[], COALESCE((${expr}) #> '${pathStr}'::text[], '{}'::jsonb), true)`;
210
+ }
211
+ // Final set with actual value
212
+ const fullPathStr = "{" + path.join(",") + "}";
213
+ expr = `jsonb_set(${expr}, '${fullPathStr}'::text[], $1::jsonb, true)`;
214
+ sql = `UPDATE ${qualifiedTable} SET "${column}" = ${expr} WHERE ${sanitizeWhereClause(where)}`;
215
+ const result = await adapter.executeQuery(sql, [toJsonString(value)]);
216
+ return {
217
+ rowsAffected: result.rowsAffected,
218
+ hint: "rowsAffected counts matched rows, not path creations",
219
+ };
220
+ }
221
+ else {
222
+ // Use COALESCE to handle NULL columns - initialize to empty object
223
+ sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_set(COALESCE("${column}", '{}'::jsonb), $1, $2::jsonb, $3) WHERE ${sanitizeWhereClause(where)}`;
224
+ const result = await adapter.executeQuery(sql, [
225
+ path,
226
+ toJsonString(value),
227
+ createFlag,
228
+ ]);
229
+ const hint = createFlag
230
+ ? "NULL columns initialized to {}; createMissing creates path if absent"
231
+ : "createMissing=false: path must exist or value won't be set";
232
+ return { rowsAffected: result.rowsAffected, hint };
233
+ }
143
234
  }
144
- // For deep nested paths with createMissing=true, build intermediate objects
145
- // PostgreSQL's jsonb_set only creates one level, so we nest calls for deep paths
146
- let sql;
147
- if (createFlag && path.length > 1) {
148
- // Build nested jsonb_set calls to ensure each intermediate path exists
149
- // Start with COALESCE to handle NULL columns
150
- let expr = `COALESCE("${column}", '{}'::jsonb)`;
151
- // For each intermediate level, wrap in jsonb_set to initialize to {}
152
- for (let i = 0; i < path.length - 1; i++) {
153
- const subPath = path.slice(0, i + 1);
154
- const pathStr = "{" + subPath.join(",") + "}";
155
- // Use COALESCE on the extraction from current expr, not original column
156
- // This properly chains the nested creation
157
- expr = `jsonb_set(${expr}, '${pathStr}'::text[], COALESCE((${expr}) #> '${pathStr}'::text[], '{}'::jsonb), true)`;
158
- }
159
- // Final set with actual value
160
- const fullPathStr = "{" + path.join(",") + "}";
161
- expr = `jsonb_set(${expr}, '${fullPathStr}'::text[], $1::jsonb, true)`;
162
- sql = `UPDATE "${table}" SET "${column}" = ${expr} WHERE ${where}`;
163
- const result = await adapter.executeQuery(sql, [toJsonString(value)]);
235
+ catch (error) {
164
236
  return {
165
- rowsAffected: result.rowsAffected,
166
- hint: "rowsAffected counts matched rows, not path creations",
237
+ success: false,
238
+ error: formatPostgresError(error, {
239
+ tool: "pg_jsonb_set",
240
+ }),
167
241
  };
168
242
  }
169
- else {
170
- // Use COALESCE to handle NULL columns - initialize to empty object
171
- sql = `UPDATE "${table}" SET "${column}" = jsonb_set(COALESCE("${column}", '{}'::jsonb), $1, $2::jsonb, $3) WHERE ${where}`;
172
- const result = await adapter.executeQuery(sql, [
173
- path,
174
- toJsonString(value),
175
- createFlag,
176
- ]);
177
- const hint = createFlag
178
- ? "NULL columns initialized to {}; createMissing creates path if absent"
179
- : "createMissing=false: path must exist or value won't be set";
180
- return { rowsAffected: result.rowsAffected, hint };
181
- }
182
243
  },
183
244
  };
184
245
  }
@@ -192,52 +253,68 @@ export function createJsonbInsertTool(adapter) {
192
253
  annotations: write("JSONB Insert"),
193
254
  icons: getToolIcons("jsonb", write("JSONB Insert")),
194
255
  handler: async (params, _context) => {
195
- const parsed = JsonbInsertSchema.parse(params);
196
- // Resolve table/column from optional aliases
197
- const table = parsed.table ?? parsed.tableName;
198
- const column = parsed.column ?? parsed.col;
199
- if (!table || !column) {
200
- throw new Error("table and column are required");
201
- }
202
- // Normalize path - convert numeric segments to numbers for PostgreSQL
203
- const path = normalizePathForInsert(parsed.path);
204
- // Validate required 'where' parameter
205
- if (!parsed.where || parsed.where.trim() === "") {
206
- throw new Error('pg_jsonb_insert requires a WHERE clause to identify rows to update. Example: where: "id = 1"');
207
- }
208
- // Check for NULL columns first - jsonb_insert requires existing array context
209
- const checkSql = `SELECT COUNT(*) as null_count FROM "${table}" WHERE ${parsed.where} AND "${column}" IS NULL`;
210
- const checkResult = await adapter.executeQuery(checkSql);
211
- const nullCount = Number(checkResult.rows?.[0]?.["null_count"] ?? 0);
212
- if (nullCount > 0) {
213
- throw new Error(`pg_jsonb_insert cannot operate on NULL columns. Use pg_jsonb_set to initialize the column first: pg_jsonb_set({table: "${table}", column: "${column}", path: "myarray", value: [], where: "..."})`);
214
- }
215
- // Validate target path points to an array, not an object
216
- // Get the parent path (one level up from where we're inserting)
217
- const parentPath = path.slice(0, -1);
218
- if (parentPath.length === 0) {
219
- // Inserting at root level - check column type
220
- const typeCheckSql = `SELECT jsonb_typeof("${column}") as type FROM "${table}" WHERE ${parsed.where} LIMIT 1`;
221
- const typeResult = await adapter.executeQuery(typeCheckSql);
222
- const columnType = typeResult.rows?.[0]?.["type"];
223
- if (columnType && columnType !== "array") {
224
- throw new Error(`pg_jsonb_insert requires an array target. Column contains '${columnType}'. Use pg_jsonb_set for objects.`);
256
+ try {
257
+ const parsed = JsonbInsertSchema.parse(params);
258
+ // Resolve table/column from optional aliases
259
+ const table = parsed.table ?? parsed.tableName;
260
+ const column = parsed.column ?? parsed.col;
261
+ if (!table || !column) {
262
+ return { success: false, error: "table and column are required" };
225
263
  }
226
- }
227
- else {
228
- // Check the parent path type
229
- const typeCheckSql = `SELECT jsonb_typeof("${column}" #> $1) as type FROM "${table}" WHERE ${parsed.where} LIMIT 1`;
230
- const parentPathStrings = parentPath.map((p) => String(p));
231
- const typeResult = await adapter.executeQuery(typeCheckSql, [
232
- parentPathStrings,
233
- ]);
234
- const targetType = typeResult.rows?.[0]?.["type"];
235
- if (targetType && targetType !== "array") {
236
- throw new Error(`pg_jsonb_insert requires an array target. Path '${parentPathStrings.join(".")}' contains '${targetType}'. Use pg_jsonb_set for objects.`);
264
+ // Validate schema and build qualified table name
265
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
266
+ if (tableError)
267
+ return tableError;
268
+ // Normalize path - convert numeric segments to numbers for PostgreSQL
269
+ const path = normalizePathForInsert(parsed.path);
270
+ // Validate required 'where' parameter
271
+ if (!parsed.where || parsed.where.trim() === "") {
272
+ return {
273
+ success: false,
274
+ error: 'pg_jsonb_insert requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
275
+ };
237
276
  }
238
- }
239
- const sql = `UPDATE "${table}" SET "${column}" = jsonb_insert("${column}", $1, $2::jsonb, $3) WHERE ${parsed.where}`;
240
- try {
277
+ // Check for NULL columns first - jsonb_insert requires existing array context
278
+ const checkSql = `SELECT COUNT(*) as null_count FROM ${qualifiedTable} WHERE ${sanitizeWhereClause(parsed.where)} AND "${column}" IS NULL`;
279
+ const checkResult = await adapter.executeQuery(checkSql);
280
+ const nullCount = Number(checkResult.rows?.[0]?.["null_count"] ?? 0);
281
+ if (nullCount > 0) {
282
+ return {
283
+ success: false,
284
+ error: `pg_jsonb_insert cannot operate on NULL columns. Use pg_jsonb_set to initialize the column first: pg_jsonb_set({table: "${table}", column: "${column}", path: "myarray", value: [], where: "..."})`,
285
+ };
286
+ }
287
+ // Validate target path points to an array, not an object
288
+ // Get the parent path (one level up from where we're inserting)
289
+ const parentPath = path.slice(0, -1);
290
+ if (parentPath.length === 0) {
291
+ // Inserting at root level - check column type
292
+ const typeCheckSql = `SELECT jsonb_typeof("${column}") as type FROM ${qualifiedTable} WHERE ${sanitizeWhereClause(parsed.where)} LIMIT 1`;
293
+ const typeResult = await adapter.executeQuery(typeCheckSql);
294
+ const columnType = typeResult.rows?.[0]?.["type"];
295
+ if (columnType && columnType !== "array") {
296
+ return {
297
+ success: false,
298
+ error: `pg_jsonb_insert requires an array target. Column contains '${columnType}'. Use pg_jsonb_set for objects.`,
299
+ };
300
+ }
301
+ }
302
+ else {
303
+ // Check the parent path type
304
+ const typeCheckSql = `SELECT jsonb_typeof("${column}" #> $1) as type FROM ${qualifiedTable} WHERE ${sanitizeWhereClause(parsed.where)} LIMIT 1`;
305
+ const parentPathStrings = parentPath.map((p) => String(p));
306
+ const typeResult = await adapter.executeQuery(typeCheckSql, [
307
+ parentPathStrings,
308
+ ]);
309
+ const targetType = typeResult.rows?.[0]?.["type"];
310
+ if (targetType && targetType !== "array") {
311
+ return {
312
+ success: false,
313
+ error: `pg_jsonb_insert requires an array target. Path '${parentPathStrings.join(".")}' contains '${targetType}'. Use pg_jsonb_set for objects.`,
314
+ };
315
+ }
316
+ }
317
+ const sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_insert("${column}", $1, $2::jsonb, $3) WHERE ${sanitizeWhereClause(parsed.where)}`;
241
318
  const result = await adapter.executeQuery(sql, [
242
319
  path,
243
320
  toJsonString(parsed.value),
@@ -246,16 +323,27 @@ export function createJsonbInsertTool(adapter) {
246
323
  return { rowsAffected: result.rowsAffected };
247
324
  }
248
325
  catch (error) {
249
- // Improve PostgreSQL error messages
326
+ // Improve specific PostgreSQL error messages
250
327
  if (error instanceof Error &&
251
328
  error.message.includes("cannot replace existing key")) {
252
- throw new Error(`pg_jsonb_insert is for arrays only. For objects, use pg_jsonb_set. If updating an existing array element, use pg_jsonb_set.`);
329
+ return {
330
+ success: false,
331
+ error: `pg_jsonb_insert is for arrays only. For objects, use pg_jsonb_set. If updating an existing array element, use pg_jsonb_set.`,
332
+ };
253
333
  }
254
334
  if (error instanceof Error &&
255
335
  error.message.includes("path element is not an integer")) {
256
- throw new Error(`pg_jsonb_insert requires numeric index for array position. Use array format with number: ["tags", 0] not ["tags", "0"] or "tags.0"`);
336
+ return {
337
+ success: false,
338
+ error: `pg_jsonb_insert requires numeric index for array position. Use array format with number: ["tags", 0] not ["tags", "0"] or "tags.0"`,
339
+ };
257
340
  }
258
- throw error;
341
+ return {
342
+ success: false,
343
+ error: formatPostgresError(error, {
344
+ tool: "pg_jsonb_insert",
345
+ }),
346
+ };
259
347
  }
260
348
  },
261
349
  };
@@ -270,62 +358,72 @@ export function createJsonbDeleteTool(adapter) {
270
358
  annotations: write("JSONB Delete"),
271
359
  icons: getToolIcons("jsonb", write("JSONB Delete")),
272
360
  handler: async (params, _context) => {
273
- const parsed = JsonbDeleteSchema.parse(params);
274
- // Resolve table/column from optional aliases
275
- const table = parsed.table ?? parsed.tableName;
276
- const column = parsed.column ?? parsed.col;
277
- if (!table || !column) {
278
- throw new Error("table and column are required");
279
- }
280
- // Validate required 'where' parameter
281
- if (!parsed.where || parsed.where.trim() === "") {
282
- throw new Error('pg_jsonb_delete requires a WHERE clause to identify rows to update. Example: where: "id = 1"');
283
- }
284
- // Validate path is not empty
285
- if (parsed.path === "" ||
286
- (Array.isArray(parsed.path) && parsed.path.length === 0)) {
287
- throw new Error("pg_jsonb_delete requires a non-empty path. Provide a key name or path to delete.");
288
- }
289
- // Determine if path should be treated as nested (array path) or single key
290
- // - Array paths: ["a", "b"], ["0"], [1]
291
- // - Bare number: 0, 1 (treat as array index)
292
- // - Dot notation: "a.b.c"
293
- // - Numeric string: "0", "1" (treat as array index)
294
- // - Single key: "mykey" (use - operator, not #-)
295
- let pathForPostgres;
296
- let useArrayOperator = false;
297
- if (typeof parsed.path === "number") {
298
- // Bare number - treat as array index
299
- pathForPostgres = [String(parsed.path)];
300
- useArrayOperator = true;
301
- }
302
- else if (Array.isArray(parsed.path)) {
303
- // Already an array - normalize to string array
304
- pathForPostgres = normalizePathToArray(parsed.path);
305
- useArrayOperator = true;
306
- }
307
- else if (parsed.path.includes(".")) {
308
- // Dot notation - convert to array
309
- pathForPostgres = parsed.path.split(".").filter((p) => p !== "");
310
- useArrayOperator = true;
311
- }
312
- else if (/^\d+$/.test(parsed.path)) {
313
- // Pure numeric string - treat as array index
314
- pathForPostgres = [parsed.path];
315
- useArrayOperator = true;
361
+ try {
362
+ const parsed = JsonbDeleteSchema.parse(params);
363
+ // Resolve table/column from optional aliases
364
+ const table = parsed.table ?? parsed.tableName;
365
+ const column = parsed.column ?? parsed.col;
366
+ if (!table || !column) {
367
+ return { success: false, error: "table and column are required" };
368
+ }
369
+ // Validate schema and build qualified table name
370
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
371
+ if (tableError)
372
+ return tableError;
373
+ // Validate required 'where' parameter
374
+ if (!parsed.where || parsed.where.trim() === "") {
375
+ return {
376
+ success: false,
377
+ error: 'pg_jsonb_delete requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
378
+ };
379
+ }
380
+ // Validate path is not empty
381
+ if (parsed.path === "" ||
382
+ (Array.isArray(parsed.path) && parsed.path.length === 0)) {
383
+ return {
384
+ success: false,
385
+ error: "pg_jsonb_delete requires a non-empty path. Provide a key name or path to delete.",
386
+ };
387
+ }
388
+ // Determine if path should be treated as nested (array path) or single key
389
+ let pathForPostgres;
390
+ let useArrayOperator;
391
+ if (typeof parsed.path === "number") {
392
+ pathForPostgres = [String(parsed.path)];
393
+ useArrayOperator = true;
394
+ }
395
+ else if (Array.isArray(parsed.path)) {
396
+ pathForPostgres = normalizePathToArray(parsed.path);
397
+ useArrayOperator = true;
398
+ }
399
+ else if (parsed.path.includes(".")) {
400
+ pathForPostgres = parsed.path.split(".").filter((p) => p !== "");
401
+ useArrayOperator = true;
402
+ }
403
+ else if (/^\d+$/.test(parsed.path)) {
404
+ pathForPostgres = [parsed.path];
405
+ useArrayOperator = true;
406
+ }
407
+ else {
408
+ pathForPostgres = parsed.path;
409
+ useArrayOperator = false;
410
+ }
411
+ const pathExpr = useArrayOperator ? `#- $1` : `- $1`;
412
+ const sql = `UPDATE ${qualifiedTable} SET "${column}" = "${column}" ${pathExpr} WHERE ${sanitizeWhereClause(parsed.where)}`;
413
+ const result = await adapter.executeQuery(sql, [pathForPostgres]);
414
+ return {
415
+ rowsAffected: result.rowsAffected,
416
+ hint: "rowsAffected counts matched rows, not whether key existed",
417
+ };
316
418
  }
317
- else {
318
- // Single key - use simple - operator
319
- pathForPostgres = parsed.path;
320
- useArrayOperator = false;
419
+ catch (error) {
420
+ return {
421
+ success: false,
422
+ error: formatPostgresError(error, {
423
+ tool: "pg_jsonb_delete",
424
+ }),
425
+ };
321
426
  }
322
- const pathExpr = useArrayOperator ? `#- $1` : `- $1`;
323
- const sql = `UPDATE "${table}" SET "${column}" = "${column}" ${pathExpr} WHERE ${parsed.where}`;
324
- const result = await adapter.executeQuery(sql, [pathForPostgres]);
325
- return {
326
- rowsAffected: result.rowsAffected,
327
- hint: "rowsAffected counts matched rows, not whether key existed",
328
- };
329
427
  },
330
428
  };
331
429
  }
@@ -339,38 +437,90 @@ export function createJsonbContainsTool(adapter) {
339
437
  annotations: readOnly("JSONB Contains"),
340
438
  icons: getToolIcons("jsonb", readOnly("JSONB Contains")),
341
439
  handler: async (params, _context) => {
342
- const parsed = JsonbContainsSchema.parse(params);
343
- // Resolve table/column from optional aliases
344
- const table = parsed.table ?? parsed.tableName;
345
- const column = parsed.column ?? parsed.col;
346
- if (!table || !column) {
347
- throw new Error("table and column are required");
440
+ try {
441
+ const parsed = JsonbContainsSchema.parse(params);
442
+ // Resolve table/column from optional aliases
443
+ const table = parsed.table ?? parsed.tableName;
444
+ const column = parsed.column ?? parsed.col;
445
+ if (!table || !column) {
446
+ return { success: false, error: "table and column are required" };
447
+ }
448
+ // Validate schema and build qualified table name
449
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
450
+ if (tableError)
451
+ return tableError;
452
+ const { select, where } = parsed;
453
+ // Parse JSON string values from MCP clients
454
+ const value = parseJsonbValue(parsed.value);
455
+ // Apply default limit (100) to prevent large payloads
456
+ const DEFAULT_LIMIT = 100;
457
+ const requestedLimit = parsed.limit;
458
+ const effectiveLimit = requestedLimit === 0 ? 0 : (requestedLimit ?? DEFAULT_LIMIT);
459
+ const selectCols = select !== undefined && select.length > 0
460
+ ? select
461
+ .map((item) => {
462
+ const { expr, alias } = parseSelectAlias(item);
463
+ // Simple column names get quoted; expressions pass through
464
+ const needsQuote = !expr.includes("->") &&
465
+ !expr.includes("(") &&
466
+ !expr.includes("::") &&
467
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(expr);
468
+ const exprStr = needsQuote ? `"${expr}"` : expr;
469
+ // Only add AS alias when an explicit alias was provided
470
+ return alias !== expr &&
471
+ alias !==
472
+ expr
473
+ .replace(/[^\w]/g, "_")
474
+ .replace(/_+/g, "_")
475
+ .replace(/^_|_$/g, "")
476
+ ? `${exprStr} AS "${alias}"`
477
+ : exprStr;
478
+ })
479
+ .join(", ")
480
+ : "*";
481
+ // Build WHERE clause combining containment check with optional filter
482
+ const containsClause = `"${column}" @> $1::jsonb`;
483
+ const whereClause = where ? ` AND ${sanitizeWhereClause(where)}` : "";
484
+ const baseSql = `SELECT ${selectCols} FROM ${qualifiedTable} WHERE ${containsClause}${whereClause}`;
485
+ // Fetch limit+1 rows to detect truncation without a separate count query
486
+ const fetchLimit = effectiveLimit > 0 ? effectiveLimit + 1 : 0;
487
+ const sql = fetchLimit > 0 ? `${baseSql} LIMIT ${String(fetchLimit)}` : baseSql;
488
+ const result = await adapter.executeQuery(sql, [toJsonString(value)]);
489
+ const allRows = result.rows ?? [];
490
+ const isTruncated = effectiveLimit > 0 && allRows.length > effectiveLimit;
491
+ const rows = isTruncated ? allRows.slice(0, effectiveLimit) : allRows;
492
+ // Warn if empty object was passed (matches all rows)
493
+ const isEmptyObject = typeof value === "object" &&
494
+ value !== null &&
495
+ !Array.isArray(value) &&
496
+ Object.keys(value).length === 0;
497
+ const response = {
498
+ rows,
499
+ count: rows.length,
500
+ };
501
+ if (isTruncated) {
502
+ response.truncated = true;
503
+ // Get exact total count
504
+ const countSql = `SELECT COUNT(*) as total FROM ${qualifiedTable} WHERE ${containsClause}${whereClause}`;
505
+ const countResult = await adapter.executeQuery(countSql, [
506
+ toJsonString(value),
507
+ ]);
508
+ response.totalCount = Number(countResult.rows?.[0]?.["total"] ?? rows.length);
509
+ }
510
+ if (isEmptyObject) {
511
+ response.warning =
512
+ "Empty {} matches ALL rows - this is PostgreSQL containment semantics";
513
+ }
514
+ return response;
348
515
  }
349
- const { select, where } = parsed;
350
- // Parse JSON string values from MCP clients
351
- const value = parseJsonbValue(parsed.value);
352
- const selectCols = select !== undefined && select.length > 0
353
- ? select.map((c) => `"${c}"`).join(", ")
354
- : "*";
355
- // Build WHERE clause combining containment check with optional filter
356
- const containsClause = `"${column}" @> $1::jsonb`;
357
- const whereClause = where ? ` AND ${where}` : "";
358
- const sql = `SELECT ${selectCols} FROM "${table}" WHERE ${containsClause}${whereClause}`;
359
- const result = await adapter.executeQuery(sql, [toJsonString(value)]);
360
- // Warn if empty object was passed (matches all rows)
361
- const isEmptyObject = typeof value === "object" &&
362
- value !== null &&
363
- !Array.isArray(value) &&
364
- Object.keys(value).length === 0;
365
- const response = {
366
- rows: result.rows,
367
- count: result.rows?.length ?? 0,
368
- };
369
- if (isEmptyObject) {
370
- response.warning =
371
- "Empty {} matches ALL rows - this is PostgreSQL containment semantics";
516
+ catch (error) {
517
+ return {
518
+ success: false,
519
+ error: formatPostgresError(error, {
520
+ tool: "pg_jsonb_contains",
521
+ }),
522
+ };
372
523
  }
373
- return response;
374
524
  },
375
525
  };
376
526
  }
@@ -384,20 +534,65 @@ export function createJsonbPathQueryTool(adapter) {
384
534
  annotations: readOnly("JSONB Path Query"),
385
535
  icons: getToolIcons("jsonb", readOnly("JSONB Path Query")),
386
536
  handler: async (params, _context) => {
387
- const parsed = JsonbPathQuerySchema.parse(params);
388
- // Resolve table/column from optional aliases
389
- const table = parsed.table ?? parsed.tableName;
390
- const column = parsed.column ?? parsed.col;
391
- if (!table || !column) {
392
- throw new Error("table and column are required");
537
+ try {
538
+ const parsed = JsonbPathQuerySchema.parse(params);
539
+ // Resolve table/column from optional aliases
540
+ const table = parsed.table ?? parsed.tableName;
541
+ const column = parsed.column ?? parsed.col;
542
+ if (!table || !column) {
543
+ return { success: false, error: "table and column are required" };
544
+ }
545
+ // Validate schema and build qualified table name
546
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
547
+ if (tableError)
548
+ return tableError;
549
+ const { path, vars, where } = parsed;
550
+ const whereClause = where ? ` WHERE ${sanitizeWhereClause(where)}` : "";
551
+ const varsJson = vars ? JSON.stringify(vars) : "{}";
552
+ // Apply default limit (100) to prevent large payloads
553
+ const DEFAULT_LIMIT = 100;
554
+ const requestedLimit = parsed.limit;
555
+ const effectiveLimit = requestedLimit === 0 ? 0 : (requestedLimit ?? DEFAULT_LIMIT);
556
+ const baseSql = `SELECT jsonb_path_query("${column}", $1::jsonpath, $2::jsonb) as result FROM ${qualifiedTable}${whereClause}`;
557
+ // Fetch limit+1 rows to detect truncation without a separate count query
558
+ const fetchLimit = effectiveLimit > 0 ? effectiveLimit + 1 : 0;
559
+ const sql = fetchLimit > 0 ? `${baseSql} LIMIT ${String(fetchLimit)}` : baseSql;
560
+ const result = await adapter.executeQuery(sql, [path, varsJson]);
561
+ const allResults = result.rows?.map((r) => r["result"]) ?? [];
562
+ const isTruncated = effectiveLimit > 0 && allResults.length > effectiveLimit;
563
+ const results = isTruncated
564
+ ? allResults.slice(0, effectiveLimit)
565
+ : allResults;
566
+ const response = { results, count: results.length };
567
+ if (isTruncated) {
568
+ response.truncated = true;
569
+ // Get exact total count
570
+ const countSql = `SELECT COUNT(*) as total FROM (SELECT jsonb_path_query("${column}", $1::jsonpath, $2::jsonb) FROM ${qualifiedTable}${whereClause}) sub`;
571
+ const countResult = await adapter.executeQuery(countSql, [
572
+ path,
573
+ varsJson,
574
+ ]);
575
+ response.totalCount = Number(countResult.rows?.[0]?.["total"] ?? results.length);
576
+ }
577
+ return response;
578
+ }
579
+ catch (error) {
580
+ // JSONPath-specific: invalid syntax
581
+ if (error instanceof Error &&
582
+ /syntax error/i.test(error.message) &&
583
+ /jsonpath/i.test(error.message)) {
584
+ return {
585
+ success: false,
586
+ error: `Invalid JSONPath syntax. Use $.key, $.array[*], or $.* ? (@.field > 10) syntax.`,
587
+ };
588
+ }
589
+ return {
590
+ success: false,
591
+ error: formatPostgresError(error, {
592
+ tool: "pg_jsonb_path_query",
593
+ }),
594
+ };
393
595
  }
394
- const { path, vars, where } = parsed;
395
- const whereClause = where ? ` WHERE ${where}` : "";
396
- const varsJson = vars ? JSON.stringify(vars) : "{}";
397
- const sql = `SELECT jsonb_path_query("${column}", $1::jsonpath, $2::jsonb) as result FROM "${table}"${whereClause}`;
398
- const result = await adapter.executeQuery(sql, [path, varsJson]);
399
- const results = result.rows?.map((r) => r["result"]);
400
- return { results, count: results?.length ?? 0 };
401
596
  },
402
597
  };
403
598
  }
@@ -433,67 +628,79 @@ export function createJsonbAggTool(adapter) {
433
628
  annotations: readOnly("JSONB Aggregate"),
434
629
  icons: getToolIcons("jsonb", readOnly("JSONB Aggregate")),
435
630
  handler: async (params, _context) => {
436
- // Parse with preprocess schema to resolve aliases (tableName→table, filter→where)
437
- const parsed = JsonbAggSchema.parse(params);
438
- const table = parsed.table;
439
- if (!table) {
440
- throw new Error("table is required");
441
- }
442
- // Build select expression with proper alias handling
443
- let selectExpr;
444
- if (parsed.select !== undefined && parsed.select.length > 0) {
445
- // Parse each select item for potential aliases
446
- const selectParts = parsed.select.map((item) => {
447
- const { expr, alias } = parseSelectAlias(item);
448
- // Detect if expression needs quoting (simple column names vs expressions)
449
- const needsQuote = !expr.includes("->") &&
450
- !expr.includes("(") &&
451
- !expr.includes("::") &&
452
- /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(expr);
453
- const exprStr = needsQuote ? `"${expr}"` : expr;
454
- return `'${alias}', ${exprStr}`;
455
- });
456
- selectExpr = `jsonb_build_object(${selectParts.join(", ")})`;
457
- }
458
- else {
459
- selectExpr = "to_jsonb(t.*)";
631
+ try {
632
+ // Parse with preprocess schema to resolve aliases (tableName→table, filter→where)
633
+ const parsed = JsonbAggSchema.parse(params);
634
+ const table = parsed.table;
635
+ if (!table) {
636
+ return { success: false, error: "table is required" };
637
+ }
638
+ // Validate schema and build qualified table name
639
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
640
+ if (tableError)
641
+ return tableError;
642
+ // Build select expression with proper alias handling
643
+ let selectExpr;
644
+ if (parsed.select !== undefined && parsed.select.length > 0) {
645
+ const selectParts = parsed.select.map((item) => {
646
+ const { expr, alias } = parseSelectAlias(item);
647
+ const needsQuote = !expr.includes("->") &&
648
+ !expr.includes("(") &&
649
+ !expr.includes("::") &&
650
+ /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(expr);
651
+ const exprStr = needsQuote ? `"${expr}"` : expr;
652
+ return `'${alias}', ${exprStr}`;
653
+ });
654
+ selectExpr = `jsonb_build_object(${selectParts.join(", ")})`;
655
+ }
656
+ else {
657
+ selectExpr = "to_jsonb(t.*)";
658
+ }
659
+ const whereClause = parsed.where
660
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
661
+ : "";
662
+ const orderByClause = parsed.orderBy
663
+ ? ` ORDER BY ${parsed.orderBy}`
664
+ : "";
665
+ const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
666
+ const hasJsonbOperator = parsed.groupBy?.includes("->") ?? false;
667
+ if (parsed.groupBy) {
668
+ const groupExpr = hasJsonbOperator
669
+ ? parsed.groupBy
670
+ : `"${parsed.groupBy}"`;
671
+ const groupClause = ` GROUP BY ${groupExpr}`;
672
+ const aggOrderBy = parsed.orderBy
673
+ ? ` ORDER BY ${parsed.orderBy}`
674
+ : "";
675
+ const sql = `SELECT ${groupExpr} as group_key, jsonb_agg(${selectExpr}${aggOrderBy}) as items FROM ${qualifiedTable} t${whereClause}${groupClause}${limitClause}`;
676
+ const result = await adapter.executeQuery(sql);
677
+ return {
678
+ result: result.rows,
679
+ count: result.rows?.length ?? 0,
680
+ grouped: true,
681
+ };
682
+ }
683
+ else {
684
+ const innerSql = `SELECT * FROM ${qualifiedTable} t${whereClause}${orderByClause}${limitClause}`;
685
+ const sql = `SELECT jsonb_agg(${selectExpr.replace(/\bt\./g, "sub.")}) as result FROM (${innerSql}) sub`;
686
+ const result = await adapter.executeQuery(sql);
687
+ const arr = result.rows?.[0]?.["result"] ?? [];
688
+ const count = Array.isArray(arr) ? arr.length : 0;
689
+ const response = { result: arr, count, grouped: false };
690
+ if (count === 0) {
691
+ response.hint = "No rows matched - returns empty array []";
692
+ }
693
+ return response;
694
+ }
460
695
  }
461
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
462
- const orderByClause = parsed.orderBy ? ` ORDER BY ${parsed.orderBy}` : "";
463
- const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
464
- // Support raw JSONB expressions (containing -> or ->> operators) without quoting
465
- const hasJsonbOperator = parsed.groupBy?.includes("->") ?? false;
466
- if (parsed.groupBy) {
467
- // Return all groups with their aggregated items
468
- // For grouped queries, we use a subquery to apply ordering before aggregation
469
- const groupExpr = hasJsonbOperator
470
- ? parsed.groupBy
471
- : `"${parsed.groupBy}"`;
472
- const groupClause = ` GROUP BY ${groupExpr}`;
473
- // Apply ordering within each group using ORDER BY inside jsonb_agg
474
- const aggOrderBy = parsed.orderBy ? ` ORDER BY ${parsed.orderBy}` : "";
475
- const sql = `SELECT ${groupExpr} as group_key, jsonb_agg(${selectExpr}${aggOrderBy}) as items FROM "${table}" t${whereClause}${groupClause}${limitClause}`;
476
- const result = await adapter.executeQuery(sql);
477
- // Return grouped result with group_key and items per group
696
+ catch (error) {
478
697
  return {
479
- result: result.rows,
480
- count: result.rows?.length ?? 0,
481
- grouped: true,
698
+ success: false,
699
+ error: formatPostgresError(error, {
700
+ tool: "pg_jsonb_agg",
701
+ }),
482
702
  };
483
703
  }
484
- else {
485
- // For non-grouped, use subquery to apply limit/order before aggregation
486
- const innerSql = `SELECT * FROM "${table}" t${whereClause}${orderByClause}${limitClause}`;
487
- const sql = `SELECT jsonb_agg(${selectExpr.replace(/\bt\./g, "sub.")}) as result FROM (${innerSql}) sub`;
488
- const result = await adapter.executeQuery(sql);
489
- const arr = result.rows?.[0]?.["result"] ?? [];
490
- const count = Array.isArray(arr) ? arr.length : 0;
491
- const response = { result: arr, count, grouped: false };
492
- if (count === 0) {
493
- response.hint = "No rows matched - returns empty array []";
494
- }
495
- return response;
496
- }
497
704
  },
498
705
  };
499
706
  }
@@ -526,22 +733,32 @@ export function createJsonbObjectTool(adapter) {
526
733
  annotations: readOnly("JSONB Object"),
527
734
  icons: getToolIcons("jsonb", readOnly("JSONB Object")),
528
735
  handler: async (params, _context) => {
529
- // Parse the input
530
- const parsed = JsonbObjectSchema.parse(params);
531
- // Support multiple parameter names: data, object, pairs (in priority order)
532
- const pairs = parsed.data ?? parsed.object ?? parsed.pairs ?? {};
533
- const entries = Object.entries(pairs);
534
- // Handle empty pairs - return empty object
535
- if (entries.length === 0) {
536
- return { object: {} };
736
+ try {
737
+ // Parse the input
738
+ const parsed = JsonbObjectSchema.parse(params);
739
+ // Support multiple parameter names: data, object, pairs (in priority order)
740
+ const pairs = parsed.data ?? parsed.object ?? parsed.pairs ?? {};
741
+ const entries = Object.entries(pairs);
742
+ // Handle empty pairs - return empty object
743
+ if (entries.length === 0) {
744
+ return { object: {} };
745
+ }
746
+ const args = entries.flatMap(([k, v]) => [k, toJsonString(v)]);
747
+ const placeholders = entries
748
+ .map((_, i) => `$${String(i * 2 + 1)}::text, $${String(i * 2 + 2)}::jsonb`)
749
+ .join(", ");
750
+ const sql = `SELECT jsonb_build_object(${placeholders}) as result`;
751
+ const result = await adapter.executeQuery(sql, args);
752
+ return { object: result.rows?.[0]?.["result"] ?? {} };
753
+ }
754
+ catch (error) {
755
+ return {
756
+ success: false,
757
+ error: formatPostgresError(error, {
758
+ tool: "pg_jsonb_object",
759
+ }),
760
+ };
537
761
  }
538
- const args = entries.flatMap(([k, v]) => [k, toJsonString(v)]);
539
- const placeholders = entries
540
- .map((_, i) => `$${String(i * 2 + 1)}::text, $${String(i * 2 + 2)}::jsonb`)
541
- .join(", ");
542
- const sql = `SELECT jsonb_build_object(${placeholders}) as result`;
543
- const result = await adapter.executeQuery(sql, args);
544
- return { object: result.rows?.[0]?.["result"] ?? {} };
545
762
  },
546
763
  };
547
764
  }
@@ -567,18 +784,28 @@ export function createJsonbArrayTool(adapter) {
567
784
  annotations: readOnly("JSONB Array"),
568
785
  icons: getToolIcons("jsonb", readOnly("JSONB Array")),
569
786
  handler: async (params, _context) => {
570
- const parsed = params;
571
- // Support both 'values' and 'elements' parameter names
572
- const values = parsed.values ?? parsed.elements ?? [];
573
- if (values.length === 0) {
574
- return { array: [] };
787
+ try {
788
+ const parsed = params;
789
+ // Support both 'values' and 'elements' parameter names
790
+ const values = parsed.values ?? parsed.elements ?? [];
791
+ if (values.length === 0) {
792
+ return { array: [] };
793
+ }
794
+ const placeholders = values
795
+ .map((_, i) => `$${String(i + 1)}::jsonb`)
796
+ .join(", ");
797
+ const sql = `SELECT jsonb_build_array(${placeholders}) as result`;
798
+ const result = await adapter.executeQuery(sql, values.map((v) => toJsonString(v)));
799
+ return { array: result.rows?.[0]?.["result"] };
800
+ }
801
+ catch (error) {
802
+ return {
803
+ success: false,
804
+ error: formatPostgresError(error, {
805
+ tool: "pg_jsonb_array",
806
+ }),
807
+ };
575
808
  }
576
- const placeholders = values
577
- .map((_, i) => `$${String(i + 1)}::jsonb`)
578
- .join(", ");
579
- const sql = `SELECT jsonb_build_array(${placeholders}) as result`;
580
- const result = await adapter.executeQuery(sql, values.map((v) => toJsonString(v)));
581
- return { array: result.rows?.[0]?.["result"] };
582
809
  },
583
810
  };
584
811
  }
@@ -592,16 +819,22 @@ export function createJsonbKeysTool(adapter) {
592
819
  annotations: readOnly("JSONB Keys"),
593
820
  icons: getToolIcons("jsonb", readOnly("JSONB Keys")),
594
821
  handler: async (params, _context) => {
595
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
596
- const parsed = JsonbKeysSchema.parse(params);
597
- const table = parsed.table;
598
- const column = parsed.column;
599
- if (!table || !column) {
600
- throw new Error("table and column are required");
601
- }
602
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
603
- const sql = `SELECT DISTINCT jsonb_object_keys("${column}") as key FROM "${table}"${whereClause}`;
604
822
  try {
823
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
824
+ const parsed = JsonbKeysSchema.parse(params);
825
+ const table = parsed.table;
826
+ const column = parsed.column;
827
+ if (!table || !column) {
828
+ return { success: false, error: "table and column are required" };
829
+ }
830
+ // Validate schema and build qualified table name
831
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
832
+ if (tableError)
833
+ return tableError;
834
+ const whereClause = parsed.where
835
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
836
+ : "";
837
+ const sql = `SELECT DISTINCT jsonb_object_keys("${column}") as key FROM ${qualifiedTable}${whereClause}`;
605
838
  const result = await adapter.executeQuery(sql);
606
839
  const keys = result.rows?.map((r) => r["key"]);
607
840
  return {
@@ -614,9 +847,17 @@ export function createJsonbKeysTool(adapter) {
614
847
  // Improve error for array columns
615
848
  if (error instanceof Error &&
616
849
  error.message.includes("cannot call jsonb_object_keys")) {
617
- throw new Error(`pg_jsonb_keys requires object columns. For array columns, use pg_jsonb_normalize with mode: 'array'.`);
850
+ return {
851
+ success: false,
852
+ error: `pg_jsonb_keys requires object columns. For array columns, use pg_jsonb_normalize with mode: 'array'.`,
853
+ };
618
854
  }
619
- throw error;
855
+ return {
856
+ success: false,
857
+ error: formatPostgresError(error, {
858
+ tool: "pg_jsonb_keys",
859
+ }),
860
+ };
620
861
  }
621
862
  },
622
863
  };
@@ -631,32 +872,63 @@ export function createJsonbStripNullsTool(adapter) {
631
872
  annotations: write("JSONB Strip Nulls"),
632
873
  icons: getToolIcons("jsonb", write("JSONB Strip Nulls")),
633
874
  handler: async (params, _context) => {
634
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
635
- const parsed = JsonbStripNullsSchema.parse(params);
636
- const table = parsed.table;
637
- const column = parsed.column;
638
- const whereClause = parsed.where;
639
- if (!table || !column) {
640
- throw new Error("table and column are required");
641
- }
642
- // Validate required 'where' parameter before SQL execution
643
- if (!whereClause || whereClause.trim() === "") {
644
- throw new Error('pg_jsonb_strip_nulls requires a WHERE clause to identify rows to update. Example: where: "id = 1"');
875
+ try {
876
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
877
+ // Wrap in try-catch to intercept Zod .refine() errors (e.g., missing WHERE)
878
+ let parsed;
879
+ try {
880
+ parsed = JsonbStripNullsSchema.parse(params);
881
+ }
882
+ catch (error) {
883
+ if (error instanceof ZodError) {
884
+ const messages = error.issues.map((i) => i.message).join("; ");
885
+ return {
886
+ success: false,
887
+ error: `pg_jsonb_strip_nulls validation error: ${messages}`,
888
+ };
889
+ }
890
+ throw error;
891
+ }
892
+ const table = parsed.table;
893
+ const column = parsed.column;
894
+ const whereClause = parsed.where;
895
+ if (!table || !column) {
896
+ return { success: false, error: "table and column are required" };
897
+ }
898
+ // Validate schema and build qualified table name
899
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
900
+ if (tableError)
901
+ return tableError;
902
+ // Validate required 'where' parameter before SQL execution
903
+ if (!whereClause || whereClause.trim() === "") {
904
+ return {
905
+ success: false,
906
+ error: 'pg_jsonb_strip_nulls requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
907
+ };
908
+ }
909
+ if (parsed.preview === true) {
910
+ // Preview mode - show before/after without modifying
911
+ const previewSql = `SELECT "${column}" as before, jsonb_strip_nulls("${column}") as after FROM ${qualifiedTable} WHERE ${sanitizeWhereClause(whereClause)}`;
912
+ const result = await adapter.executeQuery(previewSql);
913
+ return {
914
+ preview: true,
915
+ rows: result.rows,
916
+ count: result.rows?.length ?? 0,
917
+ hint: "No changes made - preview only",
918
+ };
919
+ }
920
+ const sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_strip_nulls("${column}") WHERE ${sanitizeWhereClause(whereClause)}`;
921
+ const result = await adapter.executeQuery(sql);
922
+ return { rowsAffected: result.rowsAffected };
645
923
  }
646
- if (parsed.preview === true) {
647
- // Preview mode - show before/after without modifying
648
- const previewSql = `SELECT "${column}" as before, jsonb_strip_nulls("${column}") as after FROM "${table}" WHERE ${whereClause}`;
649
- const result = await adapter.executeQuery(previewSql);
924
+ catch (error) {
650
925
  return {
651
- preview: true,
652
- rows: result.rows,
653
- count: result.rows?.length ?? 0,
654
- hint: "No changes made - preview only",
926
+ success: false,
927
+ error: formatPostgresError(error, {
928
+ tool: "pg_jsonb_strip_nulls",
929
+ }),
655
930
  };
656
931
  }
657
- const sql = `UPDATE "${table}" SET "${column}" = jsonb_strip_nulls("${column}") WHERE ${whereClause}`;
658
- const result = await adapter.executeQuery(sql);
659
- return { rowsAffected: result.rowsAffected };
660
932
  },
661
933
  };
662
934
  }
@@ -670,26 +942,42 @@ export function createJsonbTypeofTool(adapter) {
670
942
  annotations: readOnly("JSONB Typeof"),
671
943
  icons: getToolIcons("jsonb", readOnly("JSONB Typeof")),
672
944
  handler: async (params, _context) => {
673
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
674
- const parsed = JsonbTypeofSchema.parse(params);
675
- const table = parsed.table;
676
- const column = parsed.column;
677
- if (!table || !column) {
678
- throw new Error("table and column are required");
945
+ try {
946
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
947
+ const parsed = JsonbTypeofSchema.parse(params);
948
+ const table = parsed.table;
949
+ const column = parsed.column;
950
+ if (!table || !column) {
951
+ return { success: false, error: "table and column are required" };
952
+ }
953
+ // Validate schema and build qualified table name
954
+ const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
955
+ if (tableError)
956
+ return tableError;
957
+ const whereClause = parsed.where
958
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
959
+ : "";
960
+ // Normalize path to array format (accepts both string and array)
961
+ const pathArray = parsed.path !== undefined
962
+ ? normalizePathToArray(parsed.path)
963
+ : undefined;
964
+ const pathExpr = pathArray !== undefined ? ` #> $1` : "";
965
+ // Include column IS NULL check to disambiguate NULL column vs null path result
966
+ const sql = `SELECT jsonb_typeof("${column}"${pathExpr}) as type, ("${column}" IS NULL) as column_null FROM ${qualifiedTable}${whereClause}`;
967
+ const queryParams = pathArray ? [pathArray] : [];
968
+ const result = await adapter.executeQuery(sql, queryParams);
969
+ const types = result.rows?.map((r) => r["type"]);
970
+ const columnNull = result.rows?.some((r) => r["column_null"] === true) ?? false;
971
+ return { types, count: types?.length ?? 0, columnNull };
972
+ }
973
+ catch (error) {
974
+ return {
975
+ success: false,
976
+ error: formatPostgresError(error, {
977
+ tool: "pg_jsonb_typeof",
978
+ }),
979
+ };
679
980
  }
680
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
681
- // Normalize path to array format (accepts both string and array)
682
- const pathArray = parsed.path !== undefined
683
- ? normalizePathToArray(parsed.path)
684
- : undefined;
685
- const pathExpr = pathArray !== undefined ? ` #> $1` : "";
686
- // Include column IS NULL check to disambiguate NULL column vs null path result
687
- const sql = `SELECT jsonb_typeof("${column}"${pathExpr}) as type, ("${column}" IS NULL) as column_null FROM "${table}"${whereClause}`;
688
- const queryParams = pathArray ? [pathArray] : [];
689
- const result = await adapter.executeQuery(sql, queryParams);
690
- const types = result.rows?.map((r) => r["type"]);
691
- const columnNull = result.rows?.some((r) => r["column_null"] === true) ?? false;
692
- return { types, count: types?.length ?? 0, columnNull };
693
981
  },
694
982
  };
695
983
  }