@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
@@ -6,6 +6,8 @@
6
6
  import { z } from "zod";
7
7
  import { readOnly } 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";
9
11
  import { sanitizeIdentifier, sanitizeTableName, } from "../../../../utils/identifiers.js";
10
12
  import { JsonbValidatePathOutputSchema, JsonbMergeOutputSchema, JsonbNormalizeOutputSchema, JsonbDiffOutputSchema, JsonbIndexSuggestOutputSchema, JsonbSecurityScanOutputSchema, JsonbStatsOutputSchema,
11
13
  // Base schemas for MCP visibility (Split Schema pattern)
@@ -180,23 +182,30 @@ export function createJsonbMergeTool(adapter) {
180
182
  annotations: readOnly("JSONB Merge"),
181
183
  icons: getToolIcons("jsonb", readOnly("JSONB Merge")),
182
184
  handler: async (params, _context) => {
183
- const parsed = parseMergeParams(params);
184
- const useDeep = parsed.deep !== false;
185
- const useMergeArrays = parsed.mergeArrays === true;
186
- if (useDeep) {
187
- // Perform recursive deep merge in TypeScript
188
- const merged = deepMergeObjects(parsed.base, parsed.overlay, useMergeArrays);
189
- // Return the merged result directly (no PostgreSQL round-trip needed)
190
- return { merged, deep: true, mergeArrays: useMergeArrays };
185
+ try {
186
+ const parsed = parseMergeParams(params);
187
+ const useDeep = parsed.deep !== false;
188
+ const useMergeArrays = parsed.mergeArrays === true;
189
+ if (useDeep) {
190
+ const merged = deepMergeObjects(parsed.base, parsed.overlay, useMergeArrays);
191
+ return { merged, deep: true, mergeArrays: useMergeArrays };
192
+ }
193
+ else {
194
+ const sql = `SELECT $1::jsonb || $2::jsonb as result`;
195
+ const result = await adapter.executeQuery(sql, [
196
+ toJsonString(parsed.base),
197
+ toJsonString(parsed.overlay),
198
+ ]);
199
+ return { merged: result.rows?.[0]?.["result"], deep: false };
200
+ }
191
201
  }
192
- else {
193
- // Shallow merge using PostgreSQL || operator
194
- const sql = `SELECT $1::jsonb || $2::jsonb as result`;
195
- const result = await adapter.executeQuery(sql, [
196
- toJsonString(parsed.base),
197
- toJsonString(parsed.overlay),
198
- ]);
199
- return { merged: result.rows?.[0]?.["result"], deep: false };
202
+ catch (error) {
203
+ return {
204
+ success: false,
205
+ error: formatPostgresError(error, {
206
+ tool: "pg_jsonb_merge",
207
+ }),
208
+ };
200
209
  }
201
210
  },
202
211
  };
@@ -214,74 +223,86 @@ export function createJsonbNormalizeTool(adapter) {
214
223
  annotations: readOnly("JSONB Normalize"),
215
224
  icons: getToolIcons("jsonb", readOnly("JSONB Normalize")),
216
225
  handler: async (params, _context) => {
217
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
218
- const parsed = JsonbNormalizeSchema.parse(params);
219
- const table = parsed.table;
220
- const column = parsed.column;
221
- if (!table || !column) {
222
- throw new Error("table and column are required");
223
- }
224
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
225
- const mode = parsed.mode ?? "keys";
226
- // Validate mode parameter
227
- const validModes = ["keys", "array", "pairs", "flatten"];
228
- if (!validModes.includes(mode)) {
229
- throw new Error(`pg_jsonb_normalize: Invalid mode '${mode}'. Valid modes: ${validModes.join(", ")}`);
230
- }
231
- const tableName = sanitizeTableName(table);
232
- const columnName = sanitizeIdentifier(column);
233
- // Determine row identifier column
234
- let rowIdExpr;
235
- let rowIdAlias = "source_id";
236
- if (parsed.idColumn) {
237
- // User specified - use it directly
238
- rowIdExpr = sanitizeIdentifier(parsed.idColumn);
239
- }
240
- else {
241
- // Try to detect 'id' column, fall back to ctid
242
- try {
243
- const checkSql = `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'id' LIMIT 1`;
244
- const checkResult = await adapter.executeQuery(checkSql, [
245
- parsed.table,
246
- ]);
247
- if (checkResult.rows && checkResult.rows.length > 0) {
248
- rowIdExpr = '"id"';
226
+ try {
227
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
228
+ const parsed = JsonbNormalizeSchema.parse(params);
229
+ const table = parsed.table;
230
+ const column = parsed.column;
231
+ if (!table || !column) {
232
+ return { success: false, error: "table and column are required" };
233
+ }
234
+ const whereClause = parsed.where
235
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
236
+ : "";
237
+ const mode = parsed.mode ?? "keys";
238
+ // Validate mode parameter
239
+ const validModes = ["keys", "array", "pairs", "flatten"];
240
+ if (!validModes.includes(mode)) {
241
+ return {
242
+ success: false,
243
+ error: `pg_jsonb_normalize: Invalid mode '${mode}'. Valid modes: ${validModes.join(", ")}`,
244
+ };
245
+ }
246
+ // Validate schema existence for non-public schemas
247
+ const schemaName = parsed.schema ?? "public";
248
+ if (schemaName !== "public") {
249
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
250
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
251
+ return {
252
+ success: false,
253
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
254
+ };
249
255
  }
250
- else {
256
+ }
257
+ const tableName = sanitizeTableName(table, schemaName);
258
+ const columnName = sanitizeIdentifier(column);
259
+ // Determine row identifier column
260
+ let rowIdExpr;
261
+ let rowIdAlias = "source_id";
262
+ if (parsed.idColumn) {
263
+ rowIdExpr = sanitizeIdentifier(parsed.idColumn);
264
+ }
265
+ else {
266
+ try {
267
+ const checkSql = `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'id' LIMIT 1`;
268
+ const checkResult = await adapter.executeQuery(checkSql, [
269
+ parsed.table,
270
+ ]);
271
+ if (checkResult.rows && checkResult.rows.length > 0) {
272
+ rowIdExpr = '"id"';
273
+ }
274
+ else {
275
+ rowIdExpr = "ctid::text";
276
+ rowIdAlias = "source_ctid";
277
+ }
278
+ }
279
+ catch {
251
280
  rowIdExpr = "ctid::text";
252
281
  rowIdAlias = "source_ctid";
253
282
  }
254
283
  }
255
- catch {
256
- rowIdExpr = "ctid::text";
257
- rowIdAlias = "source_ctid";
284
+ let sql;
285
+ if (mode === "array") {
286
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, jsonb_array_elements(${columnName}) as element FROM ${tableName}${whereClause}`;
258
287
  }
259
- }
260
- let sql;
261
- if (mode === "array") {
262
- sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, jsonb_array_elements(${columnName}) as element FROM ${tableName}${whereClause}`;
263
- }
264
- else if (mode === "flatten") {
265
- // Recursive CTE to flatten nested objects to dot-notation keys
266
- sql = `
267
- WITH RECURSIVE
288
+ else if (mode === "flatten") {
289
+ sql = `
290
+ WITH RECURSIVE
268
291
  source_rows AS (
269
292
  SELECT ${rowIdExpr} as ${rowIdAlias}, ${columnName} as doc
270
293
  FROM ${tableName}${whereClause}
271
294
  ),
272
295
  flattened AS (
273
- -- Base case: start with top-level keys
274
- SELECT
296
+ SELECT
275
297
  sr.${rowIdAlias},
276
298
  kv.key as path,
277
299
  kv.value,
278
300
  jsonb_typeof(kv.value) as value_type
279
301
  FROM source_rows sr, jsonb_each(sr.doc) kv
280
-
302
+
281
303
  UNION ALL
282
-
283
- -- Recursive case: expand nested objects
284
- SELECT
304
+
305
+ SELECT
285
306
  f.${rowIdAlias},
286
307
  f.path || '.' || kv.key,
287
308
  kv.value,
@@ -289,27 +310,27 @@ export function createJsonbNormalizeTool(adapter) {
289
310
  FROM flattened f, jsonb_each(f.value) kv
290
311
  WHERE jsonb_typeof(f.value) = 'object'
291
312
  )
292
- SELECT ${rowIdAlias}, path as key, value, value_type FROM flattened
313
+ SELECT ${rowIdAlias}, path as key, value, value_type FROM flattened
293
314
  WHERE value_type != 'object' OR value = '{}'::jsonb
294
315
  ORDER BY ${rowIdAlias}, path
295
316
  `;
296
- }
297
- else if (mode === "pairs") {
298
- sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each(${columnName}) ${whereClause}`;
299
- }
300
- else {
301
- // Default 'keys' mode
302
- sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each_text(${columnName}) ${whereClause}`;
303
- }
304
- try {
317
+ }
318
+ else if (mode === "pairs") {
319
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each(${columnName}) ${whereClause}`;
320
+ }
321
+ else {
322
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each_text(${columnName}) ${whereClause}`;
323
+ }
305
324
  const result = await adapter.executeQuery(sql);
306
325
  // Check for empty flatten results on array columns
307
326
  if (mode === "flatten" && (result.rows?.length ?? 0) === 0) {
308
- // Verify if this is because column contains arrays
309
327
  const typeCheckSql = `SELECT jsonb_typeof(${columnName}) as type FROM ${tableName}${whereClause} LIMIT 1`;
310
328
  const typeResult = await adapter.executeQuery(typeCheckSql);
311
329
  if (typeResult.rows?.[0]?.["type"] === "array") {
312
- throw new Error(`pg_jsonb_normalize flatten mode requires object columns. Column appears to contain arrays - use 'array' mode instead.`);
330
+ return {
331
+ success: false,
332
+ error: `pg_jsonb_normalize flatten mode requires object columns. Column appears to contain arrays - use 'array' mode instead.`,
333
+ };
313
334
  }
314
335
  }
315
336
  return { rows: result.rows, count: result.rows?.length ?? 0, mode };
@@ -318,14 +339,24 @@ export function createJsonbNormalizeTool(adapter) {
318
339
  // Improve error for array columns with object-only modes
319
340
  if (error instanceof Error &&
320
341
  error.message.includes("cannot call jsonb_each")) {
321
- throw new Error(`pg_jsonb_normalize '${mode}' mode requires object columns. For array columns, use mode: 'array'.`);
342
+ return {
343
+ success: false,
344
+ error: `pg_jsonb_normalize requires object columns for this mode. For array columns, use mode: 'array'.`,
345
+ };
322
346
  }
323
- // Improve error for object columns with array mode
324
347
  if (error instanceof Error &&
325
348
  error.message.includes("cannot extract elements from an object")) {
326
- throw new Error(`pg_jsonb_normalize 'array' mode requires array columns. For object columns, use mode: 'keys' or 'pairs'.`);
349
+ return {
350
+ success: false,
351
+ error: `pg_jsonb_normalize 'array' mode requires array columns. For object columns, use mode: 'keys' or 'pairs'.`,
352
+ };
327
353
  }
328
- throw error;
354
+ return {
355
+ success: false,
356
+ error: formatPostgresError(error, {
357
+ tool: "pg_jsonb_normalize",
358
+ }),
359
+ };
329
360
  }
330
361
  },
331
362
  };
@@ -353,23 +384,26 @@ export function createJsonbDiffTool(adapter) {
353
384
  annotations: readOnly("JSONB Diff"),
354
385
  icons: getToolIcons("jsonb", readOnly("JSONB Diff")),
355
386
  handler: async (params, _context) => {
356
- let parsed;
357
387
  try {
358
- parsed = JsonbDiffSchema.parse(params);
359
- }
360
- catch {
361
- // Provide friendly error for array/non-object inputs
362
- throw new Error("pg_jsonb_diff requires two JSONB objects. Arrays and primitive values are not supported. Use {} format for both doc1 and doc2.");
363
- }
364
- const sql = `
365
- WITH
388
+ let parsed;
389
+ try {
390
+ parsed = JsonbDiffSchema.parse(params);
391
+ }
392
+ catch {
393
+ return {
394
+ success: false,
395
+ error: "pg_jsonb_diff requires two JSONB objects. Arrays and primitive values are not supported. Use {} format for both doc1 and doc2.",
396
+ };
397
+ }
398
+ const sql = `
399
+ WITH
366
400
  j1 AS (SELECT key, value FROM jsonb_each($1::jsonb)),
367
401
  j2 AS (SELECT key, value FROM jsonb_each($2::jsonb))
368
- SELECT
402
+ SELECT
369
403
  COALESCE(j1.key, j2.key) as key,
370
404
  j1.value as value1,
371
405
  j2.value as value2,
372
- CASE
406
+ CASE
373
407
  WHEN j1.key IS NULL THEN 'added'
374
408
  WHEN j2.key IS NULL THEN 'removed'
375
409
  WHEN j1.value = j2.value THEN 'unchanged'
@@ -378,16 +412,25 @@ export function createJsonbDiffTool(adapter) {
378
412
  FROM j1 FULL OUTER JOIN j2 ON j1.key = j2.key
379
413
  WHERE j1.value IS DISTINCT FROM j2.value
380
414
  `;
381
- const result = await adapter.executeQuery(sql, [
382
- toJsonString(parsed.doc1),
383
- toJsonString(parsed.doc2),
384
- ]);
385
- return {
386
- differences: result.rows,
387
- hasDifferences: (result.rows?.length ?? 0) > 0,
388
- comparison: "shallow",
389
- hint: "Compares top-level keys only. Nested object changes show as modified.",
390
- };
415
+ const result = await adapter.executeQuery(sql, [
416
+ toJsonString(parsed.doc1),
417
+ toJsonString(parsed.doc2),
418
+ ]);
419
+ return {
420
+ differences: result.rows,
421
+ hasDifferences: (result.rows?.length ?? 0) > 0,
422
+ comparison: "shallow",
423
+ hint: "Compares top-level keys only. Nested object changes show as modified.",
424
+ };
425
+ }
426
+ catch (error) {
427
+ return {
428
+ success: false,
429
+ error: formatPostgresError(error, {
430
+ tool: "pg_jsonb_diff",
431
+ }),
432
+ };
433
+ }
391
434
  },
392
435
  };
393
436
  }
@@ -404,86 +447,102 @@ export function createJsonbIndexSuggestTool(adapter) {
404
447
  annotations: readOnly("JSONB Index Suggest"),
405
448
  icons: getToolIcons("jsonb", readOnly("JSONB Index Suggest")),
406
449
  handler: async (params, _context) => {
407
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
408
- const parsed = JsonbIndexSuggestSchema.parse(params);
409
- const table = parsed.table;
410
- const column = parsed.column;
411
- if (!table || !column) {
412
- throw new Error("table and column are required");
413
- }
414
- const sample = parsed.sampleSize ?? 1000;
415
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
416
- const tableName = sanitizeTableName(table);
417
- const columnName = sanitizeIdentifier(column);
418
- const keySql = `
419
- SELECT key, COUNT(*) as frequency,
450
+ try {
451
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
452
+ const parsed = JsonbIndexSuggestSchema.parse(params);
453
+ const table = parsed.table;
454
+ const column = parsed.column;
455
+ if (!table || !column) {
456
+ return { success: false, error: "table and column are required" };
457
+ }
458
+ const sample = parsed.sampleSize ?? 1000;
459
+ const whereClause = parsed.where
460
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
461
+ : "";
462
+ // Validate schema existence for non-public schemas
463
+ const schemaName = parsed.schema ?? "public";
464
+ if (schemaName !== "public") {
465
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
466
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
467
+ return {
468
+ success: false,
469
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
470
+ };
471
+ }
472
+ }
473
+ const tableName = sanitizeTableName(table, schemaName);
474
+ const columnName = sanitizeIdentifier(column);
475
+ const keySql = `
476
+ SELECT key, COUNT(*) as frequency,
420
477
  jsonb_typeof(value) as value_type
421
478
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
422
- jsonb_each(${columnName})
479
+ jsonb_each(${columnName})
423
480
  GROUP BY key, jsonb_typeof(value)
424
481
  ORDER BY frequency DESC
425
482
  LIMIT 20
426
483
  `;
427
- let keyResult;
428
- try {
429
- keyResult = await adapter.executeQuery(keySql);
430
- }
431
- catch (error) {
432
- if (error instanceof Error &&
433
- (error.message.includes("function jsonb_each") ||
434
- error.message.includes("cannot call jsonb_each"))) {
435
- throw new Error(`pg_jsonb_index_suggest requires JSONB objects (not arrays). Column '${column}' may not be JSONB type or contains arrays.`);
436
- }
437
- throw error;
438
- }
439
- const indexSql = `
484
+ const keyResult = await adapter.executeQuery(keySql);
485
+ const indexSql = `
440
486
  SELECT indexname, indexdef
441
487
  FROM pg_indexes
442
488
  WHERE tablename = $1
443
489
  AND indexdef LIKE '%' || $2 || '%'
444
490
  `;
445
- const indexResult = await adapter.executeQuery(indexSql, [
446
- parsed.table,
447
- parsed.column,
448
- ]);
449
- const recommendations = [];
450
- // Cast frequency to number (PostgreSQL returns bigint as string)
451
- const keys = (keyResult.rows ?? []).map((row) => ({
452
- key: row["key"],
453
- frequency: Number(row["frequency"]),
454
- value_type: row["value_type"],
455
- }));
456
- // Only recommend GIN index if there's data to analyze
457
- if ((indexResult.rows?.length ?? 0) === 0 && keys.length > 0) {
458
- recommendations.push(`CREATE INDEX ON ${tableName} USING GIN (${columnName})`);
459
- }
460
- for (const keyInfo of keys.slice(0, 5)) {
461
- if (keyInfo.frequency > sample * 0.5) {
462
- recommendations.push(`CREATE INDEX ON ${tableName} ((${columnName} ->> '${keyInfo.key.replace(/'/g, "''")}'))`);
491
+ const indexResult = await adapter.executeQuery(indexSql, [
492
+ parsed.table,
493
+ parsed.column,
494
+ ]);
495
+ const recommendations = [];
496
+ const keys = (keyResult.rows ?? []).map((row) => ({
497
+ key: row["key"],
498
+ frequency: Number(row["frequency"]),
499
+ value_type: row["value_type"],
500
+ }));
501
+ if ((indexResult.rows?.length ?? 0) === 0 && keys.length > 0) {
502
+ recommendations.push(`CREATE INDEX ON ${tableName} USING GIN (${columnName})`);
463
503
  }
464
- }
465
- // Build response with helpful hints
466
- const response = {
467
- keyDistribution: keys,
468
- existingIndexes: indexResult.rows,
469
- recommendations,
470
- };
471
- // Add explanation when no recommendations
472
- if (recommendations.length === 0) {
473
- if ((indexResult.rows?.length ?? 0) > 0) {
474
- response.hint =
475
- "No new recommendations - existing indexes already cover this column";
476
- }
477
- else if (keys.length === 0) {
478
- response.hint =
479
- "No recommendations - table is empty or column has no keys to analyze";
504
+ for (const keyInfo of keys.slice(0, 5)) {
505
+ if (keyInfo.frequency > sample * 0.5) {
506
+ recommendations.push(`CREATE INDEX ON ${tableName} ((${columnName} ->> '${keyInfo.key.replace(/'/g, "''")}'))`);
507
+ }
480
508
  }
481
- else {
482
- response.hint =
483
- "No recommendations - no keys appeared in >50% of sampled rows";
509
+ const response = {
510
+ keyDistribution: keys,
511
+ existingIndexes: indexResult.rows,
512
+ recommendations,
513
+ };
514
+ if (recommendations.length === 0) {
515
+ if ((indexResult.rows?.length ?? 0) > 0) {
516
+ response.hint =
517
+ "No new recommendations - existing indexes already cover this column";
518
+ }
519
+ else if (keys.length === 0) {
520
+ response.hint =
521
+ "No recommendations - table is empty or column has no keys to analyze";
522
+ }
523
+ else {
524
+ response.hint =
525
+ "No recommendations - no keys appeared in >50% of sampled rows";
526
+ }
484
527
  }
528
+ return response;
529
+ }
530
+ catch (error) {
531
+ if (error instanceof Error &&
532
+ (error.message.includes("function jsonb_each") ||
533
+ error.message.includes("cannot call jsonb_each"))) {
534
+ return {
535
+ success: false,
536
+ error: `pg_jsonb_index_suggest requires JSONB objects (not arrays). Column may not be JSONB type or contains arrays.`,
537
+ };
538
+ }
539
+ return {
540
+ success: false,
541
+ error: formatPostgresError(error, {
542
+ tool: "pg_jsonb_index_suggest",
543
+ }),
544
+ };
485
545
  }
486
- return response;
487
546
  },
488
547
  };
489
548
  }
@@ -500,85 +559,105 @@ export function createJsonbSecurityScanTool(adapter) {
500
559
  annotations: readOnly("JSONB Security Scan"),
501
560
  icons: getToolIcons("jsonb", readOnly("JSONB Security Scan")),
502
561
  handler: async (params, _context) => {
503
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
504
- const parsed = JsonbSecurityScanSchema.parse(params);
505
- const table = parsed.table;
506
- const column = parsed.column;
507
- if (!table || !column) {
508
- throw new Error("table and column are required");
509
- }
510
- const sample = parsed.sampleSize ?? 100;
511
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
512
- const issues = [];
513
- const tableName = sanitizeTableName(table);
514
- const columnName = sanitizeIdentifier(column);
515
- // Count actual rows scanned (may be less than sample if table is small)
516
- const countSql = `SELECT COUNT(*) as count FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t`;
517
- const countResult = await adapter.executeQuery(countSql);
518
- const actualRowsScanned = Number(countResult.rows?.[0]?.["count"] ?? 0);
519
- const sensitiveKeysSql = `
562
+ try {
563
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
564
+ const parsed = JsonbSecurityScanSchema.parse(params);
565
+ const table = parsed.table;
566
+ const column = parsed.column;
567
+ if (!table || !column) {
568
+ return { success: false, error: "table and column are required" };
569
+ }
570
+ const sample = parsed.sampleSize ?? 100;
571
+ const whereClause = parsed.where
572
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
573
+ : "";
574
+ const issues = [];
575
+ // Validate schema existence for non-public schemas
576
+ const schemaName = parsed.schema ?? "public";
577
+ if (schemaName !== "public") {
578
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
579
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
580
+ return {
581
+ success: false,
582
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
583
+ };
584
+ }
585
+ }
586
+ const tableName = sanitizeTableName(table, schemaName);
587
+ const columnName = sanitizeIdentifier(column);
588
+ // Count actual rows scanned
589
+ const countSql = `SELECT COUNT(*) as count FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t`;
590
+ const countResult = await adapter.executeQuery(countSql);
591
+ const actualRowsScanned = Number(countResult.rows?.[0]?.["count"] ?? 0);
592
+ const sensitiveKeysSql = `
520
593
  SELECT key, COUNT(*) as count
521
594
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
522
595
  jsonb_each_text(${columnName})
523
- WHERE lower(key) IN ('password', 'secret', 'token', 'api_key', 'apikey',
596
+ WHERE lower(key) IN ('password', 'secret', 'token', 'api_key', 'apikey',
524
597
  'auth', 'credential', 'ssn', 'credit_card', 'cvv')
525
598
  GROUP BY key
526
599
  `;
527
- let sensitiveResult;
528
- try {
529
- sensitiveResult = await adapter.executeQuery(sensitiveKeysSql);
530
- }
531
- catch (error) {
532
- if (error instanceof Error &&
533
- (error.message.includes("function jsonb_each") ||
534
- error.message.includes("cannot call jsonb_each"))) {
535
- throw new Error(`pg_jsonb_security_scan requires JSONB objects. Column '${column}' may contain arrays or non-JSONB data.`);
600
+ const sensitiveResult = await adapter.executeQuery(sensitiveKeysSql);
601
+ for (const row of (sensitiveResult.rows ?? [])) {
602
+ issues.push({
603
+ type: "sensitive_key",
604
+ key: row.key,
605
+ count: Number(row.count),
606
+ });
536
607
  }
537
- throw error;
538
- }
539
- for (const row of (sensitiveResult.rows ?? [])) {
540
- issues.push({
541
- type: "sensitive_key",
542
- key: row.key,
543
- count: Number(row.count),
544
- });
545
- }
546
- const injectionSql = `
608
+ const injectionSql = `
547
609
  SELECT key, COUNT(*) as count
548
610
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
549
611
  jsonb_each_text(${columnName})
550
612
  WHERE value ~* '(\\bSELECT\\s+.+\\bFROM\\b|\\bINSERT\\s+INTO\\b|\\bUPDATE\\s+.+\\bSET\\b|\\bDELETE\\s+FROM\\b|\\bDROP\\s+(TABLE|DATABASE|INDEX)\\b|\\bUNION\\s+(ALL\\s+)?SELECT\\b|--\\s*$|;\\s*(SELECT|INSERT|UPDATE|DELETE))'
551
613
  GROUP BY key
552
614
  `;
553
- const injectionResult = await adapter.executeQuery(injectionSql);
554
- for (const row of (injectionResult.rows ?? [])) {
555
- issues.push({
556
- type: "sql_injection_pattern",
557
- key: row.key,
558
- count: Number(row.count),
559
- });
560
- }
561
- // XSS pattern detection
562
- const xssSql = `
615
+ const injectionResult = await adapter.executeQuery(injectionSql);
616
+ for (const row of (injectionResult.rows ?? [])) {
617
+ issues.push({
618
+ type: "sql_injection_pattern",
619
+ key: row.key,
620
+ count: Number(row.count),
621
+ });
622
+ }
623
+ // XSS pattern detection
624
+ const xssSql = `
563
625
  SELECT key, COUNT(*) as count
564
626
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
565
627
  jsonb_each_text(${columnName})
566
628
  WHERE value ~* '(<script|javascript:|on(click|load|error|mouseover)\\s*=|<iframe|<object|<embed|<svg[^>]+on|<img[^>]+onerror)'
567
629
  GROUP BY key
568
630
  `;
569
- const xssResult = await adapter.executeQuery(xssSql);
570
- for (const row of (xssResult.rows ?? [])) {
571
- issues.push({
572
- type: "xss_pattern",
573
- key: row.key,
574
- count: Number(row.count),
575
- });
631
+ const xssResult = await adapter.executeQuery(xssSql);
632
+ for (const row of (xssResult.rows ?? [])) {
633
+ issues.push({
634
+ type: "xss_pattern",
635
+ key: row.key,
636
+ count: Number(row.count),
637
+ });
638
+ }
639
+ return {
640
+ scannedRows: actualRowsScanned,
641
+ issues,
642
+ riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
643
+ };
644
+ }
645
+ catch (error) {
646
+ if (error instanceof Error &&
647
+ (error.message.includes("function jsonb_each") ||
648
+ error.message.includes("cannot call jsonb_each"))) {
649
+ return {
650
+ success: false,
651
+ error: `pg_jsonb_security_scan requires JSONB objects. Column may contain arrays or non-JSONB data.`,
652
+ };
653
+ }
654
+ return {
655
+ success: false,
656
+ error: formatPostgresError(error, {
657
+ tool: "pg_jsonb_security_scan",
658
+ }),
659
+ };
576
660
  }
577
- return {
578
- scannedRows: actualRowsScanned,
579
- issues,
580
- riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
581
- };
582
661
  },
583
662
  };
584
663
  }
@@ -595,38 +674,51 @@ export function createJsonbStatsTool(adapter) {
595
674
  annotations: readOnly("JSONB Stats"),
596
675
  icons: getToolIcons("jsonb", readOnly("JSONB Stats")),
597
676
  handler: async (params, _context) => {
598
- // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
599
- const parsed = JsonbStatsSchema.parse(params);
600
- const table = parsed.table;
601
- const column = parsed.column;
602
- if (!table || !column) {
603
- throw new Error("table and column are required");
604
- }
605
- const sample = parsed.sampleSize ?? 1000;
606
- const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
607
- const tableName = sanitizeTableName(table);
608
- const columnName = sanitizeIdentifier(column);
609
- const basicSql = `
610
- SELECT
677
+ try {
678
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
679
+ const parsed = JsonbStatsSchema.parse(params);
680
+ const table = parsed.table;
681
+ const column = parsed.column;
682
+ if (!table || !column) {
683
+ return { success: false, error: "table and column are required" };
684
+ }
685
+ const sample = parsed.sampleSize ?? 1000;
686
+ const whereClause = parsed.where
687
+ ? ` WHERE ${sanitizeWhereClause(parsed.where)}`
688
+ : "";
689
+ // Validate schema existence for non-public schemas
690
+ const schemaName = parsed.schema ?? "public";
691
+ if (schemaName !== "public") {
692
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
693
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
694
+ return {
695
+ success: false,
696
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
697
+ };
698
+ }
699
+ }
700
+ const tableName = sanitizeTableName(table, schemaName);
701
+ const columnName = sanitizeIdentifier(column);
702
+ const basicSql = `
703
+ SELECT
611
704
  COUNT(*) as total_rows,
612
705
  COUNT(${columnName}) as non_null_count,
613
706
  AVG(length(${columnName}::text))::int as avg_size_bytes,
614
707
  MAX(length(${columnName}::text)) as max_size_bytes
615
708
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
616
709
  `;
617
- const basicResult = await adapter.executeQuery(basicSql);
618
- // Cast bigint values to numbers (PostgreSQL returns bigint as string)
619
- const basics = basicResult.rows?.[0];
620
- const basicsNormalized = basics
621
- ? {
622
- total_rows: Number(basics["total_rows"]),
623
- non_null_count: Number(basics["non_null_count"]),
624
- avg_size_bytes: Number(basics["avg_size_bytes"]),
625
- max_size_bytes: Number(basics["max_size_bytes"]),
626
- }
627
- : undefined;
628
- const keyLimit = parsed.topKeysLimit ?? 20;
629
- const keySql = `
710
+ const basicResult = await adapter.executeQuery(basicSql);
711
+ const basics = basicResult.rows?.[0];
712
+ const basicsNormalized = basics
713
+ ? {
714
+ total_rows: Number(basics["total_rows"]),
715
+ non_null_count: Number(basics["non_null_count"]),
716
+ avg_size_bytes: Number(basics["avg_size_bytes"]),
717
+ max_size_bytes: Number(basics["max_size_bytes"]),
718
+ }
719
+ : undefined;
720
+ const keyLimit = parsed.topKeysLimit ?? 20;
721
+ const keySql = `
630
722
  SELECT key, COUNT(*) as frequency
631
723
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
632
724
  jsonb_object_keys(${columnName}) key
@@ -634,57 +726,62 @@ export function createJsonbStatsTool(adapter) {
634
726
  ORDER BY frequency DESC
635
727
  LIMIT ${String(keyLimit)}
636
728
  `;
637
- let topKeys = [];
638
- try {
639
- const keyResult = await adapter.executeQuery(keySql);
640
- // Cast frequency to number
641
- topKeys = (keyResult.rows ?? []).map((row) => ({
642
- key: row["key"],
643
- frequency: Number(row["frequency"]),
644
- }));
645
- }
646
- catch (error) {
647
- // Gracefully handle array columns (jsonb_object_keys fails on arrays)
648
- if (error instanceof Error &&
649
- error.message.includes("cannot call jsonb_object_keys")) {
650
- // Leave topKeys empty for array columns - this is valid
729
+ let topKeys = [];
730
+ try {
731
+ const keyResult = await adapter.executeQuery(keySql);
732
+ topKeys = (keyResult.rows ?? []).map((row) => ({
733
+ key: row["key"],
734
+ frequency: Number(row["frequency"]),
735
+ }));
651
736
  }
652
- else {
653
- throw error;
737
+ catch (error) {
738
+ // Gracefully handle array columns (jsonb_object_keys fails on arrays)
739
+ if (error instanceof Error &&
740
+ error.message.includes("cannot call jsonb_object_keys")) {
741
+ // Leave topKeys empty for array columns - this is valid
742
+ }
743
+ else {
744
+ throw error; // Re-throw to be caught by outer catch
745
+ }
654
746
  }
655
- }
656
- const typeSql = `
747
+ const typeSql = `
657
748
  SELECT jsonb_typeof(${columnName}) as type, COUNT(*) as count
658
749
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
659
750
  GROUP BY jsonb_typeof(${columnName})
660
751
  `;
661
- const typeResult = await adapter.executeQuery(typeSql);
662
- // Cast count to number
663
- const typeDistribution = (typeResult.rows ?? []).map((row) => ({
664
- type: row["type"],
665
- count: Number(row["count"]),
666
- }));
667
- // Calculate SQL NULL count for disambiguation
668
- const sqlNullCount = typeDistribution.find((t) => t.type === null)?.count ?? 0;
669
- const hasNullColumns = sqlNullCount > 0;
670
- const isArrayColumn = typeDistribution.some((t) => t.type === "array");
671
- // Determine appropriate hint
672
- let hint;
673
- if (hasNullColumns) {
674
- hint =
675
- "typeDistribution null type represents SQL NULL columns, not JSON null values";
752
+ const typeResult = await adapter.executeQuery(typeSql);
753
+ const typeDistribution = (typeResult.rows ?? []).map((row) => ({
754
+ type: row["type"],
755
+ count: Number(row["count"]),
756
+ }));
757
+ const sqlNullCount = typeDistribution.find((t) => t.type === null)?.count ?? 0;
758
+ const hasNullColumns = sqlNullCount > 0;
759
+ const isArrayColumn = typeDistribution.some((t) => t.type === "array");
760
+ let hint;
761
+ if (hasNullColumns) {
762
+ hint =
763
+ "typeDistribution null type represents SQL NULL columns, not JSON null values";
764
+ }
765
+ else if (topKeys.length === 0 && isArrayColumn) {
766
+ hint =
767
+ 'topKeys empty for array columns - use pg_jsonb_normalize mode: "array" to analyze elements';
768
+ }
769
+ return {
770
+ basics: basicsNormalized,
771
+ topKeys,
772
+ typeDistribution,
773
+ sqlNullCount,
774
+ hint,
775
+ };
676
776
  }
677
- else if (topKeys.length === 0 && isArrayColumn) {
678
- hint =
679
- 'topKeys empty for array columns - use pg_jsonb_normalize mode: "array" to analyze elements';
777
+ catch (error) {
778
+ return {
779
+ success: false,
780
+ error: formatPostgresError(error, {
781
+ tool: "pg_jsonb_stats",
782
+ }),
783
+ };
680
784
  }
681
- return {
682
- basics: basicsNormalized,
683
- topKeys,
684
- typeDistribution,
685
- sqlNullCount,
686
- hint,
687
- };
688
785
  },
689
786
  };
690
787
  }