@neverinfamous/postgres-mcp 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +95 -81
  2. package/dist/__tests__/mocks/adapter.d.ts.map +1 -1
  3. package/dist/__tests__/mocks/adapter.js +0 -1
  4. package/dist/__tests__/mocks/adapter.js.map +1 -1
  5. package/dist/__tests__/mocks/pool.d.ts.map +1 -1
  6. package/dist/__tests__/mocks/pool.js +0 -1
  7. package/dist/__tests__/mocks/pool.js.map +1 -1
  8. package/dist/adapters/DatabaseAdapter.js +1 -1
  9. package/dist/adapters/DatabaseAdapter.js.map +1 -1
  10. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  11. package/dist/adapters/postgresql/PostgresAdapter.js +78 -8
  12. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  13. package/dist/adapters/postgresql/prompts/backup.d.ts.map +1 -1
  14. package/dist/adapters/postgresql/prompts/backup.js +2 -3
  15. package/dist/adapters/postgresql/prompts/backup.js.map +1 -1
  16. package/dist/adapters/postgresql/prompts/citext.d.ts.map +1 -1
  17. package/dist/adapters/postgresql/prompts/citext.js +3 -4
  18. package/dist/adapters/postgresql/prompts/citext.js.map +1 -1
  19. package/dist/adapters/postgresql/prompts/extensionSetup.d.ts.map +1 -1
  20. package/dist/adapters/postgresql/prompts/extensionSetup.js +2 -3
  21. package/dist/adapters/postgresql/prompts/extensionSetup.js.map +1 -1
  22. package/dist/adapters/postgresql/prompts/health.d.ts.map +1 -1
  23. package/dist/adapters/postgresql/prompts/health.js +2 -3
  24. package/dist/adapters/postgresql/prompts/health.js.map +1 -1
  25. package/dist/adapters/postgresql/prompts/index.js +20 -27
  26. package/dist/adapters/postgresql/prompts/index.js.map +1 -1
  27. package/dist/adapters/postgresql/prompts/indexTuning.d.ts.map +1 -1
  28. package/dist/adapters/postgresql/prompts/indexTuning.js +2 -3
  29. package/dist/adapters/postgresql/prompts/indexTuning.js.map +1 -1
  30. package/dist/adapters/postgresql/prompts/kcache.d.ts.map +1 -1
  31. package/dist/adapters/postgresql/prompts/kcache.js +3 -4
  32. package/dist/adapters/postgresql/prompts/kcache.js.map +1 -1
  33. package/dist/adapters/postgresql/prompts/ltree.d.ts.map +1 -1
  34. package/dist/adapters/postgresql/prompts/ltree.js +5 -6
  35. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  36. package/dist/adapters/postgresql/prompts/partman.d.ts.map +1 -1
  37. package/dist/adapters/postgresql/prompts/partman.js +2 -3
  38. package/dist/adapters/postgresql/prompts/partman.js.map +1 -1
  39. package/dist/adapters/postgresql/prompts/pgcron.d.ts.map +1 -1
  40. package/dist/adapters/postgresql/prompts/pgcron.js +2 -3
  41. package/dist/adapters/postgresql/prompts/pgcron.js.map +1 -1
  42. package/dist/adapters/postgresql/prompts/pgcrypto.d.ts.map +1 -1
  43. package/dist/adapters/postgresql/prompts/pgcrypto.js +3 -4
  44. package/dist/adapters/postgresql/prompts/pgcrypto.js.map +1 -1
  45. package/dist/adapters/postgresql/prompts/pgvector.d.ts.map +1 -1
  46. package/dist/adapters/postgresql/prompts/pgvector.js +3 -4
  47. package/dist/adapters/postgresql/prompts/pgvector.js.map +1 -1
  48. package/dist/adapters/postgresql/prompts/postgis.d.ts.map +1 -1
  49. package/dist/adapters/postgresql/prompts/postgis.js +2 -3
  50. package/dist/adapters/postgresql/prompts/postgis.js.map +1 -1
  51. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  52. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  53. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  54. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  55. package/dist/adapters/postgresql/schemas/backup.d.ts +8 -4
  56. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  57. package/dist/adapters/postgresql/schemas/backup.js +11 -4
  58. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  59. package/dist/adapters/postgresql/schemas/core.d.ts +54 -19
  60. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  61. package/dist/adapters/postgresql/schemas/core.js +65 -17
  62. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  63. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  64. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  65. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  66. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  67. package/dist/adapters/postgresql/schemas/extensions.d.ts +168 -73
  68. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  69. package/dist/adapters/postgresql/schemas/extensions.js +179 -62
  70. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  71. package/dist/adapters/postgresql/schemas/index.d.ts +5 -5
  72. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  73. package/dist/adapters/postgresql/schemas/index.js +9 -7
  74. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  75. package/dist/adapters/postgresql/schemas/jsonb.d.ts +94 -42
  76. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  77. package/dist/adapters/postgresql/schemas/jsonb.js +101 -30
  78. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  79. package/dist/adapters/postgresql/schemas/monitoring.d.ts +28 -11
  80. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  81. package/dist/adapters/postgresql/schemas/monitoring.js +49 -24
  82. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  83. package/dist/adapters/postgresql/schemas/partitioning.d.ts +15 -11
  84. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  85. package/dist/adapters/postgresql/schemas/partitioning.js +17 -13
  86. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  87. package/dist/adapters/postgresql/schemas/performance.d.ts +62 -31
  88. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  89. package/dist/adapters/postgresql/schemas/performance.js +86 -24
  90. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  91. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  92. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  93. package/dist/adapters/postgresql/schemas/postgis.js +20 -3
  94. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  95. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +35 -23
  96. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  97. package/dist/adapters/postgresql/schemas/schema-mgmt.js +69 -26
  98. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  99. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  100. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  101. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  102. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  103. package/dist/adapters/postgresql/schemas/text-search.d.ts +8 -5
  104. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  105. package/dist/adapters/postgresql/schemas/text-search.js +15 -5
  106. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  107. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  108. package/dist/adapters/postgresql/tools/admin.js +211 -140
  109. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  110. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  111. package/dist/adapters/postgresql/tools/backup/dump.js +410 -387
  112. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  113. package/dist/adapters/postgresql/tools/backup/planning.d.ts.map +1 -1
  114. package/dist/adapters/postgresql/tools/backup/planning.js +175 -172
  115. package/dist/adapters/postgresql/tools/backup/planning.js.map +1 -1
  116. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  117. package/dist/adapters/postgresql/tools/citext.js +221 -163
  118. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  119. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  120. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  121. package/dist/adapters/postgresql/tools/core/convenience.js +96 -9
  122. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  123. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  124. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  125. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  126. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  127. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  128. package/dist/adapters/postgresql/tools/core/health.js +23 -6
  129. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  130. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  131. package/dist/adapters/postgresql/tools/core/indexes.js +45 -4
  132. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  133. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  134. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  135. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  136. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  137. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  138. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  139. package/dist/adapters/postgresql/tools/core/schemas.d.ts +52 -25
  140. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  141. package/dist/adapters/postgresql/tools/core/schemas.js +55 -25
  142. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  143. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  144. package/dist/adapters/postgresql/tools/core/tables.js +74 -30
  145. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  146. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  147. package/dist/adapters/postgresql/tools/cron.js +274 -179
  148. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  149. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  150. package/dist/adapters/postgresql/tools/jsonb/advanced.js +372 -284
  151. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  152. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  153. package/dist/adapters/postgresql/tools/jsonb/basic.js +617 -398
  154. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  155. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  156. package/dist/adapters/postgresql/tools/kcache.js +282 -220
  157. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  158. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  159. package/dist/adapters/postgresql/tools/ltree.js +126 -35
  160. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  161. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  162. package/dist/adapters/postgresql/tools/monitoring.js +59 -40
  163. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  164. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  165. package/dist/adapters/postgresql/tools/partitioning.js +150 -15
  166. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  167. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  168. package/dist/adapters/postgresql/tools/partman/management.js +12 -5
  169. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  170. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  171. package/dist/adapters/postgresql/tools/partman/operations.js +135 -22
  172. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  173. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  174. package/dist/adapters/postgresql/tools/performance/analysis.js +264 -160
  175. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  176. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  177. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  178. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  179. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  180. package/dist/adapters/postgresql/tools/performance/monitoring.js +44 -7
  181. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  182. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  183. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  184. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  185. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  186. package/dist/adapters/postgresql/tools/performance/stats.js +128 -37
  187. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  188. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  189. package/dist/adapters/postgresql/tools/pgcrypto.js +242 -87
  190. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  191. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  192. package/dist/adapters/postgresql/tools/postgis/advanced.js +293 -201
  193. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  194. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  195. package/dist/adapters/postgresql/tools/postgis/basic.js +359 -249
  196. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  197. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  198. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  199. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  200. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  201. package/dist/adapters/postgresql/tools/schema.js +515 -226
  202. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  203. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  204. package/dist/adapters/postgresql/tools/stats/advanced.js +515 -476
  205. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  206. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  207. package/dist/adapters/postgresql/tools/stats/basic.js +302 -293
  208. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  209. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  210. package/dist/adapters/postgresql/tools/text.js +398 -220
  211. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  212. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  213. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  214. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  215. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  216. package/dist/adapters/postgresql/tools/vector/advanced.js +70 -38
  217. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  218. package/dist/adapters/postgresql/tools/vector/basic.d.ts +8 -0
  219. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  220. package/dist/adapters/postgresql/tools/vector/basic.js +194 -82
  221. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  222. package/dist/cli/args.d.ts +2 -0
  223. package/dist/cli/args.d.ts.map +1 -1
  224. package/dist/cli/args.js +15 -0
  225. package/dist/cli/args.js.map +1 -1
  226. package/dist/cli.js +7 -6
  227. package/dist/cli.js.map +1 -1
  228. package/dist/codemode/api.d.ts.map +1 -1
  229. package/dist/codemode/api.js +4 -3
  230. package/dist/codemode/api.js.map +1 -1
  231. package/dist/constants/ServerInstructions.d.ts +1 -1
  232. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  233. package/dist/constants/ServerInstructions.js +76 -34
  234. package/dist/constants/ServerInstructions.js.map +1 -1
  235. package/dist/filtering/ToolConstants.d.ts +29 -13
  236. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  237. package/dist/filtering/ToolConstants.js +44 -27
  238. package/dist/filtering/ToolConstants.js.map +1 -1
  239. package/dist/utils/logger.js +2 -2
  240. package/dist/utils/logger.js.map +1 -1
  241. package/dist/utils/progress-utils.js +1 -1
  242. package/dist/utils/progress-utils.js.map +1 -1
  243. package/package.json +13 -9
@@ -6,6 +6,7 @@
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";
9
10
  import { sanitizeIdentifier, sanitizeTableName, } from "../../../../utils/identifiers.js";
10
11
  import { JsonbValidatePathOutputSchema, JsonbMergeOutputSchema, JsonbNormalizeOutputSchema, JsonbDiffOutputSchema, JsonbIndexSuggestOutputSchema, JsonbSecurityScanOutputSchema, JsonbStatsOutputSchema,
11
12
  // Base schemas for MCP visibility (Split Schema pattern)
@@ -180,23 +181,30 @@ export function createJsonbMergeTool(adapter) {
180
181
  annotations: readOnly("JSONB Merge"),
181
182
  icons: getToolIcons("jsonb", readOnly("JSONB Merge")),
182
183
  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 };
184
+ try {
185
+ const parsed = parseMergeParams(params);
186
+ const useDeep = parsed.deep !== false;
187
+ const useMergeArrays = parsed.mergeArrays === true;
188
+ if (useDeep) {
189
+ const merged = deepMergeObjects(parsed.base, parsed.overlay, useMergeArrays);
190
+ return { merged, deep: true, mergeArrays: useMergeArrays };
191
+ }
192
+ else {
193
+ const sql = `SELECT $1::jsonb || $2::jsonb as result`;
194
+ const result = await adapter.executeQuery(sql, [
195
+ toJsonString(parsed.base),
196
+ toJsonString(parsed.overlay),
197
+ ]);
198
+ return { merged: result.rows?.[0]?.["result"], deep: false };
199
+ }
191
200
  }
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 };
201
+ catch (error) {
202
+ return {
203
+ success: false,
204
+ error: formatPostgresError(error, {
205
+ tool: "pg_jsonb_merge",
206
+ }),
207
+ };
200
208
  }
201
209
  },
202
210
  };
@@ -214,63 +222,74 @@ export function createJsonbNormalizeTool(adapter) {
214
222
  annotations: readOnly("JSONB Normalize"),
215
223
  icons: getToolIcons("jsonb", readOnly("JSONB Normalize")),
216
224
  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"';
225
+ try {
226
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
227
+ const parsed = JsonbNormalizeSchema.parse(params);
228
+ const table = parsed.table;
229
+ const column = parsed.column;
230
+ if (!table || !column) {
231
+ return { success: false, error: "table and column are required" };
232
+ }
233
+ const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
234
+ const mode = parsed.mode ?? "keys";
235
+ // Validate mode parameter
236
+ const validModes = ["keys", "array", "pairs", "flatten"];
237
+ if (!validModes.includes(mode)) {
238
+ return {
239
+ success: false,
240
+ error: `pg_jsonb_normalize: Invalid mode '${mode}'. Valid modes: ${validModes.join(", ")}`,
241
+ };
242
+ }
243
+ // Validate schema existence for non-public schemas
244
+ const schemaName = parsed.schema ?? "public";
245
+ if (schemaName !== "public") {
246
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
247
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
248
+ return {
249
+ success: false,
250
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
251
+ };
249
252
  }
250
- else {
253
+ }
254
+ const tableName = sanitizeTableName(table, schemaName);
255
+ const columnName = sanitizeIdentifier(column);
256
+ // Determine row identifier column
257
+ let rowIdExpr;
258
+ let rowIdAlias = "source_id";
259
+ if (parsed.idColumn) {
260
+ rowIdExpr = sanitizeIdentifier(parsed.idColumn);
261
+ }
262
+ else {
263
+ try {
264
+ const checkSql = `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'id' LIMIT 1`;
265
+ const checkResult = await adapter.executeQuery(checkSql, [
266
+ parsed.table,
267
+ ]);
268
+ if (checkResult.rows && checkResult.rows.length > 0) {
269
+ rowIdExpr = '"id"';
270
+ }
271
+ else {
272
+ rowIdExpr = "ctid::text";
273
+ rowIdAlias = "source_ctid";
274
+ }
275
+ }
276
+ catch {
251
277
  rowIdExpr = "ctid::text";
252
278
  rowIdAlias = "source_ctid";
253
279
  }
254
280
  }
255
- catch {
256
- rowIdExpr = "ctid::text";
257
- rowIdAlias = "source_ctid";
281
+ let sql;
282
+ if (mode === "array") {
283
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, jsonb_array_elements(${columnName}) as element FROM ${tableName}${whereClause}`;
258
284
  }
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 = `
285
+ else if (mode === "flatten") {
286
+ sql = `
267
287
  WITH RECURSIVE
268
288
  source_rows AS (
269
289
  SELECT ${rowIdExpr} as ${rowIdAlias}, ${columnName} as doc
270
290
  FROM ${tableName}${whereClause}
271
291
  ),
272
292
  flattened AS (
273
- -- Base case: start with top-level keys
274
293
  SELECT
275
294
  sr.${rowIdAlias},
276
295
  kv.key as path,
@@ -280,7 +299,6 @@ export function createJsonbNormalizeTool(adapter) {
280
299
 
281
300
  UNION ALL
282
301
 
283
- -- Recursive case: expand nested objects
284
302
  SELECT
285
303
  f.${rowIdAlias},
286
304
  f.path || '.' || kv.key,
@@ -293,23 +311,23 @@ export function createJsonbNormalizeTool(adapter) {
293
311
  WHERE value_type != 'object' OR value = '{}'::jsonb
294
312
  ORDER BY ${rowIdAlias}, path
295
313
  `;
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 {
314
+ }
315
+ else if (mode === "pairs") {
316
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each(${columnName}) ${whereClause}`;
317
+ }
318
+ else {
319
+ sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each_text(${columnName}) ${whereClause}`;
320
+ }
305
321
  const result = await adapter.executeQuery(sql);
306
322
  // Check for empty flatten results on array columns
307
323
  if (mode === "flatten" && (result.rows?.length ?? 0) === 0) {
308
- // Verify if this is because column contains arrays
309
324
  const typeCheckSql = `SELECT jsonb_typeof(${columnName}) as type FROM ${tableName}${whereClause} LIMIT 1`;
310
325
  const typeResult = await adapter.executeQuery(typeCheckSql);
311
326
  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.`);
327
+ return {
328
+ success: false,
329
+ error: `pg_jsonb_normalize flatten mode requires object columns. Column appears to contain arrays - use 'array' mode instead.`,
330
+ };
313
331
  }
314
332
  }
315
333
  return { rows: result.rows, count: result.rows?.length ?? 0, mode };
@@ -318,14 +336,24 @@ export function createJsonbNormalizeTool(adapter) {
318
336
  // Improve error for array columns with object-only modes
319
337
  if (error instanceof Error &&
320
338
  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'.`);
339
+ return {
340
+ success: false,
341
+ error: `pg_jsonb_normalize requires object columns for this mode. For array columns, use mode: 'array'.`,
342
+ };
322
343
  }
323
- // Improve error for object columns with array mode
324
344
  if (error instanceof Error &&
325
345
  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'.`);
346
+ return {
347
+ success: false,
348
+ error: `pg_jsonb_normalize 'array' mode requires array columns. For object columns, use mode: 'keys' or 'pairs'.`,
349
+ };
327
350
  }
328
- throw error;
351
+ return {
352
+ success: false,
353
+ error: formatPostgresError(error, {
354
+ tool: "pg_jsonb_normalize",
355
+ }),
356
+ };
329
357
  }
330
358
  },
331
359
  };
@@ -353,15 +381,18 @@ export function createJsonbDiffTool(adapter) {
353
381
  annotations: readOnly("JSONB Diff"),
354
382
  icons: getToolIcons("jsonb", readOnly("JSONB Diff")),
355
383
  handler: async (params, _context) => {
356
- let parsed;
357
384
  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 = `
385
+ let parsed;
386
+ try {
387
+ parsed = JsonbDiffSchema.parse(params);
388
+ }
389
+ catch {
390
+ return {
391
+ success: false,
392
+ error: "pg_jsonb_diff requires two JSONB objects. Arrays and primitive values are not supported. Use {} format for both doc1 and doc2.",
393
+ };
394
+ }
395
+ const sql = `
365
396
  WITH
366
397
  j1 AS (SELECT key, value FROM jsonb_each($1::jsonb)),
367
398
  j2 AS (SELECT key, value FROM jsonb_each($2::jsonb))
@@ -378,16 +409,25 @@ export function createJsonbDiffTool(adapter) {
378
409
  FROM j1 FULL OUTER JOIN j2 ON j1.key = j2.key
379
410
  WHERE j1.value IS DISTINCT FROM j2.value
380
411
  `;
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
- };
412
+ const result = await adapter.executeQuery(sql, [
413
+ toJsonString(parsed.doc1),
414
+ toJsonString(parsed.doc2),
415
+ ]);
416
+ return {
417
+ differences: result.rows,
418
+ hasDifferences: (result.rows?.length ?? 0) > 0,
419
+ comparison: "shallow",
420
+ hint: "Compares top-level keys only. Nested object changes show as modified.",
421
+ };
422
+ }
423
+ catch (error) {
424
+ return {
425
+ success: false,
426
+ error: formatPostgresError(error, {
427
+ tool: "pg_jsonb_diff",
428
+ }),
429
+ };
430
+ }
391
431
  },
392
432
  };
393
433
  }
@@ -404,18 +444,30 @@ export function createJsonbIndexSuggestTool(adapter) {
404
444
  annotations: readOnly("JSONB Index Suggest"),
405
445
  icons: getToolIcons("jsonb", readOnly("JSONB Index Suggest")),
406
446
  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 = `
447
+ try {
448
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
449
+ const parsed = JsonbIndexSuggestSchema.parse(params);
450
+ const table = parsed.table;
451
+ const column = parsed.column;
452
+ if (!table || !column) {
453
+ return { success: false, error: "table and column are required" };
454
+ }
455
+ const sample = parsed.sampleSize ?? 1000;
456
+ const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
457
+ // Validate schema existence for non-public schemas
458
+ const schemaName = parsed.schema ?? "public";
459
+ if (schemaName !== "public") {
460
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
461
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
462
+ return {
463
+ success: false,
464
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
465
+ };
466
+ }
467
+ }
468
+ const tableName = sanitizeTableName(table, schemaName);
469
+ const columnName = sanitizeIdentifier(column);
470
+ const keySql = `
419
471
  SELECT key, COUNT(*) as frequency,
420
472
  jsonb_typeof(value) as value_type
421
473
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
@@ -424,66 +476,68 @@ export function createJsonbIndexSuggestTool(adapter) {
424
476
  ORDER BY frequency DESC
425
477
  LIMIT 20
426
478
  `;
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 = `
479
+ const keyResult = await adapter.executeQuery(keySql);
480
+ const indexSql = `
440
481
  SELECT indexname, indexdef
441
482
  FROM pg_indexes
442
483
  WHERE tablename = $1
443
484
  AND indexdef LIKE '%' || $2 || '%'
444
485
  `;
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, "''")}'))`);
486
+ const indexResult = await adapter.executeQuery(indexSql, [
487
+ parsed.table,
488
+ parsed.column,
489
+ ]);
490
+ const recommendations = [];
491
+ const keys = (keyResult.rows ?? []).map((row) => ({
492
+ key: row["key"],
493
+ frequency: Number(row["frequency"]),
494
+ value_type: row["value_type"],
495
+ }));
496
+ if ((indexResult.rows?.length ?? 0) === 0 && keys.length > 0) {
497
+ recommendations.push(`CREATE INDEX ON ${tableName} USING GIN (${columnName})`);
463
498
  }
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";
499
+ for (const keyInfo of keys.slice(0, 5)) {
500
+ if (keyInfo.frequency > sample * 0.5) {
501
+ recommendations.push(`CREATE INDEX ON ${tableName} ((${columnName} ->> '${keyInfo.key.replace(/'/g, "''")}'))`);
502
+ }
480
503
  }
481
- else {
482
- response.hint =
483
- "No recommendations - no keys appeared in >50% of sampled rows";
504
+ const response = {
505
+ keyDistribution: keys,
506
+ existingIndexes: indexResult.rows,
507
+ recommendations,
508
+ };
509
+ if (recommendations.length === 0) {
510
+ if ((indexResult.rows?.length ?? 0) > 0) {
511
+ response.hint =
512
+ "No new recommendations - existing indexes already cover this column";
513
+ }
514
+ else if (keys.length === 0) {
515
+ response.hint =
516
+ "No recommendations - table is empty or column has no keys to analyze";
517
+ }
518
+ else {
519
+ response.hint =
520
+ "No recommendations - no keys appeared in >50% of sampled rows";
521
+ }
484
522
  }
523
+ return response;
524
+ }
525
+ catch (error) {
526
+ if (error instanceof Error &&
527
+ (error.message.includes("function jsonb_each") ||
528
+ error.message.includes("cannot call jsonb_each"))) {
529
+ return {
530
+ success: false,
531
+ error: `pg_jsonb_index_suggest requires JSONB objects (not arrays). Column may not be JSONB type or contains arrays.`,
532
+ };
533
+ }
534
+ return {
535
+ success: false,
536
+ error: formatPostgresError(error, {
537
+ tool: "pg_jsonb_index_suggest",
538
+ }),
539
+ };
485
540
  }
486
- return response;
487
541
  },
488
542
  };
489
543
  }
@@ -500,23 +554,35 @@ export function createJsonbSecurityScanTool(adapter) {
500
554
  annotations: readOnly("JSONB Security Scan"),
501
555
  icons: getToolIcons("jsonb", readOnly("JSONB Security Scan")),
502
556
  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 = `
557
+ try {
558
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
559
+ const parsed = JsonbSecurityScanSchema.parse(params);
560
+ const table = parsed.table;
561
+ const column = parsed.column;
562
+ if (!table || !column) {
563
+ return { success: false, error: "table and column are required" };
564
+ }
565
+ const sample = parsed.sampleSize ?? 100;
566
+ const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
567
+ const issues = [];
568
+ // Validate schema existence for non-public schemas
569
+ const schemaName = parsed.schema ?? "public";
570
+ if (schemaName !== "public") {
571
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
572
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
573
+ return {
574
+ success: false,
575
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
576
+ };
577
+ }
578
+ }
579
+ const tableName = sanitizeTableName(table, schemaName);
580
+ const columnName = sanitizeIdentifier(column);
581
+ // Count actual rows scanned
582
+ const countSql = `SELECT COUNT(*) as count FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t`;
583
+ const countResult = await adapter.executeQuery(countSql);
584
+ const actualRowsScanned = Number(countResult.rows?.[0]?.["count"] ?? 0);
585
+ const sensitiveKeysSql = `
520
586
  SELECT key, COUNT(*) as count
521
587
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
522
588
  jsonb_each_text(${columnName})
@@ -524,61 +590,67 @@ export function createJsonbSecurityScanTool(adapter) {
524
590
  'auth', 'credential', 'ssn', 'credit_card', 'cvv')
525
591
  GROUP BY key
526
592
  `;
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.`);
593
+ const sensitiveResult = await adapter.executeQuery(sensitiveKeysSql);
594
+ for (const row of (sensitiveResult.rows ?? [])) {
595
+ issues.push({
596
+ type: "sensitive_key",
597
+ key: row.key,
598
+ count: Number(row.count),
599
+ });
536
600
  }
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 = `
601
+ const injectionSql = `
547
602
  SELECT key, COUNT(*) as count
548
603
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
549
604
  jsonb_each_text(${columnName})
550
605
  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
606
  GROUP BY key
552
607
  `;
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 = `
608
+ const injectionResult = await adapter.executeQuery(injectionSql);
609
+ for (const row of (injectionResult.rows ?? [])) {
610
+ issues.push({
611
+ type: "sql_injection_pattern",
612
+ key: row.key,
613
+ count: Number(row.count),
614
+ });
615
+ }
616
+ // XSS pattern detection
617
+ const xssSql = `
563
618
  SELECT key, COUNT(*) as count
564
619
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
565
620
  jsonb_each_text(${columnName})
566
621
  WHERE value ~* '(<script|javascript:|on(click|load|error|mouseover)\\s*=|<iframe|<object|<embed|<svg[^>]+on|<img[^>]+onerror)'
567
622
  GROUP BY key
568
623
  `;
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
- });
624
+ const xssResult = await adapter.executeQuery(xssSql);
625
+ for (const row of (xssResult.rows ?? [])) {
626
+ issues.push({
627
+ type: "xss_pattern",
628
+ key: row.key,
629
+ count: Number(row.count),
630
+ });
631
+ }
632
+ return {
633
+ scannedRows: actualRowsScanned,
634
+ issues,
635
+ riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
636
+ };
637
+ }
638
+ catch (error) {
639
+ if (error instanceof Error &&
640
+ (error.message.includes("function jsonb_each") ||
641
+ error.message.includes("cannot call jsonb_each"))) {
642
+ return {
643
+ success: false,
644
+ error: `pg_jsonb_security_scan requires JSONB objects. Column may contain arrays or non-JSONB data.`,
645
+ };
646
+ }
647
+ return {
648
+ success: false,
649
+ error: formatPostgresError(error, {
650
+ tool: "pg_jsonb_security_scan",
651
+ }),
652
+ };
576
653
  }
577
- return {
578
- scannedRows: actualRowsScanned,
579
- issues,
580
- riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
581
- };
582
654
  },
583
655
  };
584
656
  }
@@ -595,18 +667,30 @@ export function createJsonbStatsTool(adapter) {
595
667
  annotations: readOnly("JSONB Stats"),
596
668
  icons: getToolIcons("jsonb", readOnly("JSONB Stats")),
597
669
  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 = `
670
+ try {
671
+ // Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
672
+ const parsed = JsonbStatsSchema.parse(params);
673
+ const table = parsed.table;
674
+ const column = parsed.column;
675
+ if (!table || !column) {
676
+ return { success: false, error: "table and column are required" };
677
+ }
678
+ const sample = parsed.sampleSize ?? 1000;
679
+ const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
680
+ // Validate schema existence for non-public schemas
681
+ const schemaName = parsed.schema ?? "public";
682
+ if (schemaName !== "public") {
683
+ const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
684
+ if (!schemaResult.rows || schemaResult.rows.length === 0) {
685
+ return {
686
+ success: false,
687
+ error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
688
+ };
689
+ }
690
+ }
691
+ const tableName = sanitizeTableName(table, schemaName);
692
+ const columnName = sanitizeIdentifier(column);
693
+ const basicSql = `
610
694
  SELECT
611
695
  COUNT(*) as total_rows,
612
696
  COUNT(${columnName}) as non_null_count,
@@ -614,19 +698,18 @@ export function createJsonbStatsTool(adapter) {
614
698
  MAX(length(${columnName}::text)) as max_size_bytes
615
699
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
616
700
  `;
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 = `
701
+ const basicResult = await adapter.executeQuery(basicSql);
702
+ const basics = basicResult.rows?.[0];
703
+ const basicsNormalized = basics
704
+ ? {
705
+ total_rows: Number(basics["total_rows"]),
706
+ non_null_count: Number(basics["non_null_count"]),
707
+ avg_size_bytes: Number(basics["avg_size_bytes"]),
708
+ max_size_bytes: Number(basics["max_size_bytes"]),
709
+ }
710
+ : undefined;
711
+ const keyLimit = parsed.topKeysLimit ?? 20;
712
+ const keySql = `
630
713
  SELECT key, COUNT(*) as frequency
631
714
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
632
715
  jsonb_object_keys(${columnName}) key
@@ -634,57 +717,62 @@ export function createJsonbStatsTool(adapter) {
634
717
  ORDER BY frequency DESC
635
718
  LIMIT ${String(keyLimit)}
636
719
  `;
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
720
+ let topKeys = [];
721
+ try {
722
+ const keyResult = await adapter.executeQuery(keySql);
723
+ topKeys = (keyResult.rows ?? []).map((row) => ({
724
+ key: row["key"],
725
+ frequency: Number(row["frequency"]),
726
+ }));
651
727
  }
652
- else {
653
- throw error;
728
+ catch (error) {
729
+ // Gracefully handle array columns (jsonb_object_keys fails on arrays)
730
+ if (error instanceof Error &&
731
+ error.message.includes("cannot call jsonb_object_keys")) {
732
+ // Leave topKeys empty for array columns - this is valid
733
+ }
734
+ else {
735
+ throw error; // Re-throw to be caught by outer catch
736
+ }
654
737
  }
655
- }
656
- const typeSql = `
738
+ const typeSql = `
657
739
  SELECT jsonb_typeof(${columnName}) as type, COUNT(*) as count
658
740
  FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
659
741
  GROUP BY jsonb_typeof(${columnName})
660
742
  `;
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";
743
+ const typeResult = await adapter.executeQuery(typeSql);
744
+ const typeDistribution = (typeResult.rows ?? []).map((row) => ({
745
+ type: row["type"],
746
+ count: Number(row["count"]),
747
+ }));
748
+ const sqlNullCount = typeDistribution.find((t) => t.type === null)?.count ?? 0;
749
+ const hasNullColumns = sqlNullCount > 0;
750
+ const isArrayColumn = typeDistribution.some((t) => t.type === "array");
751
+ let hint;
752
+ if (hasNullColumns) {
753
+ hint =
754
+ "typeDistribution null type represents SQL NULL columns, not JSON null values";
755
+ }
756
+ else if (topKeys.length === 0 && isArrayColumn) {
757
+ hint =
758
+ 'topKeys empty for array columns - use pg_jsonb_normalize mode: "array" to analyze elements';
759
+ }
760
+ return {
761
+ basics: basicsNormalized,
762
+ topKeys,
763
+ typeDistribution,
764
+ sqlNullCount,
765
+ hint,
766
+ };
676
767
  }
677
- else if (topKeys.length === 0 && isArrayColumn) {
678
- hint =
679
- 'topKeys empty for array columns - use pg_jsonb_normalize mode: "array" to analyze elements';
768
+ catch (error) {
769
+ return {
770
+ success: false,
771
+ error: formatPostgresError(error, {
772
+ tool: "pg_jsonb_stats",
773
+ }),
774
+ };
680
775
  }
681
- return {
682
- basics: basicsNormalized,
683
- topKeys,
684
- typeDistribution,
685
- sqlNullCount,
686
- hint,
687
- };
688
776
  },
689
777
  };
690
778
  }