@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
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { readOnly } from "../../../../utils/annotations.js";
7
7
  import { getToolIcons } from "../../../../utils/icons.js";
8
+ import { formatPostgresError } from "../core/error-helpers.js";
8
9
  import {
9
10
  // Base schemas for MCP visibility
10
11
  StatsTimeSeriesSchemaBase, StatsDistributionSchemaBase, StatsHypothesisSchemaBase, StatsSamplingSchemaBase,
@@ -236,115 +237,118 @@ export function createStatsTimeSeriesTool(adapter) {
236
237
  annotations: readOnly("Time Series Analysis"),
237
238
  icons: getToolIcons("stats", readOnly("Time Series Analysis")),
238
239
  handler: async (params, _context) => {
239
- const { table, valueColumn, timeColumn, interval, aggregation, schema, where, params: queryParams, limit, groupBy, groupLimit, } = StatsTimeSeriesSchema.parse(params);
240
- const schemaPrefix = schema ? `"${schema}".` : "";
241
- const whereClause = where ? `WHERE ${where}` : "";
242
- const agg = aggregation ?? "avg";
243
- // Handle limit: undefined uses default (100), 0 means no limit
244
- // Track whether user explicitly provided a limit
245
- const userProvidedLimit = limit !== undefined;
246
- const DEFAULT_LIMIT = 100;
247
- // limit === 0 means "no limit", otherwise use provided limit or default
248
- const effectiveLimit = limit === 0 ? undefined : (limit ?? DEFAULT_LIMIT);
249
- const usingDefaultLimit = !userProvidedLimit && effectiveLimit !== undefined;
250
- // First check if table exists
251
- const schemaName = schema ?? "public";
252
- const tableCheckQuery = `
240
+ try {
241
+ const { table, valueColumn, timeColumn, interval, aggregation, schema, where, params: queryParams, limit, groupBy, groupLimit, } = StatsTimeSeriesSchema.parse(params);
242
+ const schemaPrefix = schema ? `"${schema}".` : "";
243
+ const whereClause = where ? `WHERE ${where}` : "";
244
+ const agg = aggregation ?? "avg";
245
+ // Handle limit: undefined uses default (100), 0 means no limit
246
+ // Track whether user explicitly provided a limit
247
+ const userProvidedLimit = limit !== undefined;
248
+ const DEFAULT_LIMIT = 100;
249
+ // limit === 0 means "no limit", otherwise use provided limit or default
250
+ const effectiveLimit = limit === 0 ? undefined : (limit ?? DEFAULT_LIMIT);
251
+ const usingDefaultLimit = !userProvidedLimit && effectiveLimit !== undefined;
252
+ // First check if table exists
253
+ const schemaName = schema ?? "public";
254
+ const tableCheckQuery = `
253
255
  SELECT 1 FROM information_schema.tables
254
256
  WHERE table_schema = '${schemaName}' AND table_name = '${table}'
255
257
  `;
256
- const tableCheckResult = await adapter.executeQuery(tableCheckQuery);
257
- if (tableCheckResult.rows?.length === 0) {
258
- throw new Error(`Table "${schemaName}.${table}" not found`);
259
- }
260
- // Validate timeColumn is a timestamp/date type
261
- const typeCheckQuery = `
258
+ const tableCheckResult = await adapter.executeQuery(tableCheckQuery);
259
+ if (tableCheckResult.rows?.length === 0) {
260
+ throw new Error(`Table "${schemaName}.${table}" not found`);
261
+ }
262
+ // Validate timeColumn is a timestamp/date type
263
+ const typeCheckQuery = `
262
264
  SELECT data_type
263
265
  FROM information_schema.columns
264
266
  WHERE table_schema = '${schemaName}'
265
267
  AND table_name = '${table}'
266
268
  AND column_name = '${timeColumn}'
267
269
  `;
268
- const typeResult = await adapter.executeQuery(typeCheckQuery);
269
- const typeRow = typeResult.rows?.[0];
270
- if (!typeRow) {
271
- throw new Error(`Column "${timeColumn}" not found in table "${schemaName}.${table}"`);
272
- }
273
- const validTypes = [
274
- "timestamp without time zone",
275
- "timestamp with time zone",
276
- "date",
277
- "time",
278
- "time without time zone",
279
- "time with time zone",
280
- ];
281
- if (!validTypes.includes(typeRow.data_type)) {
282
- throw new Error(`Column "${timeColumn}" is type "${typeRow.data_type}" but must be a timestamp or date type for time series analysis`);
283
- }
284
- // Note: schemaName already defined above for table check
285
- // Validate valueColumn exists and is numeric
286
- const numericTypes = [
287
- "integer",
288
- "bigint",
289
- "smallint",
290
- "numeric",
291
- "decimal",
292
- "real",
293
- "double precision",
294
- "money",
295
- ];
296
- const valueTypeQuery = `
270
+ const typeResult = await adapter.executeQuery(typeCheckQuery);
271
+ const typeRow = typeResult.rows?.[0];
272
+ if (!typeRow) {
273
+ throw new Error(`Column "${timeColumn}" not found in table "${schemaName}.${table}"`);
274
+ }
275
+ const validTypes = [
276
+ "timestamp without time zone",
277
+ "timestamp with time zone",
278
+ "date",
279
+ "time",
280
+ "time without time zone",
281
+ "time with time zone",
282
+ ];
283
+ if (!validTypes.includes(typeRow.data_type)) {
284
+ throw new Error(`Column "${timeColumn}" is type "${typeRow.data_type}" but must be a timestamp or date type for time series analysis`);
285
+ }
286
+ // Note: schemaName already defined above for table check
287
+ // Validate valueColumn exists and is numeric
288
+ const numericTypes = [
289
+ "integer",
290
+ "bigint",
291
+ "smallint",
292
+ "numeric",
293
+ "decimal",
294
+ "real",
295
+ "double precision",
296
+ "money",
297
+ ];
298
+ const valueTypeQuery = `
297
299
  SELECT data_type
298
300
  FROM information_schema.columns
299
301
  WHERE table_schema = '${schemaName}'
300
302
  AND table_name = '${table}'
301
303
  AND column_name = '${valueColumn}'
302
304
  `;
303
- const valueTypeResult = await adapter.executeQuery(valueTypeQuery);
304
- const valueTypeRow = valueTypeResult.rows?.[0];
305
- if (!valueTypeRow) {
306
- throw new Error(`Column "${valueColumn}" not found in table "${schemaName}.${table}"`);
307
- }
308
- if (!numericTypes.includes(valueTypeRow.data_type)) {
309
- throw new Error(`Column "${valueColumn}" is type "${valueTypeRow.data_type}" but must be a numeric type for time series aggregation`);
310
- }
311
- // Helper to map bucket row - convert Date to ISO string for JSON Schema
312
- // Handles both Date objects (from real DB) and strings (from mocks)
313
- const mapBucket = (row) => {
314
- const timeBucketValue = row["time_bucket"];
315
- let timeBucket;
316
- if (timeBucketValue instanceof Date) {
317
- timeBucket = timeBucketValue.toISOString();
305
+ const valueTypeResult = await adapter.executeQuery(valueTypeQuery);
306
+ const valueTypeRow = valueTypeResult.rows?.[0];
307
+ if (!valueTypeRow) {
308
+ throw new Error(`Column "${valueColumn}" not found in table "${schemaName}.${table}"`);
318
309
  }
319
- else if (typeof timeBucketValue === "string") {
320
- timeBucket = timeBucketValue;
321
- }
322
- else {
323
- // Fallback: null, undefined, or unexpected type
324
- timeBucket = "";
310
+ if (!numericTypes.includes(valueTypeRow.data_type)) {
311
+ throw new Error(`Column "${valueColumn}" is type "${valueTypeRow.data_type}" but must be a numeric type for time series aggregation`);
325
312
  }
326
- return {
327
- timeBucket,
328
- value: Number(row["value"]),
329
- count: Number(row["count"]),
313
+ // Helper to map bucket row - convert Date to ISO string for JSON Schema
314
+ // Handles both Date objects (from real DB) and strings (from mocks)
315
+ const mapBucket = (row) => {
316
+ const timeBucketValue = row["time_bucket"];
317
+ let timeBucket;
318
+ if (timeBucketValue instanceof Date) {
319
+ timeBucket = timeBucketValue.toISOString();
320
+ }
321
+ else if (typeof timeBucketValue === "string") {
322
+ timeBucket = timeBucketValue;
323
+ }
324
+ else {
325
+ // Fallback: null, undefined, or unexpected type
326
+ timeBucket = "";
327
+ }
328
+ return {
329
+ timeBucket,
330
+ value: Number(row["value"]),
331
+ count: Number(row["count"]),
332
+ };
330
333
  };
331
- };
332
- if (groupBy !== undefined) {
333
- // Handle groupLimit: undefined uses default (20), 0 means no limit
334
- const DEFAULT_GROUP_LIMIT = 20;
335
- const userProvidedGroupLimit = groupLimit !== undefined;
336
- const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
337
- // First get total count of distinct groups for truncation indicator
338
- const groupCountSql = `
339
- SELECT COUNT(DISTINCT "${groupBy}") as total_groups
334
+ if (groupBy !== undefined) {
335
+ // Handle groupLimit: undefined uses default (20), 0 means no limit
336
+ const DEFAULT_GROUP_LIMIT = 20;
337
+ const userProvidedGroupLimit = groupLimit !== undefined;
338
+ const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
339
+ // First get total count of distinct groups for truncation indicator
340
+ // COUNT(DISTINCT) excludes NULLs per SQL standard, so add 1 if any NULLs exist
341
+ const groupCountSql = `
342
+ SELECT COUNT(DISTINCT "${groupBy}") +
343
+ CASE WHEN COUNT(*) > COUNT("${groupBy}") THEN 1 ELSE 0 END as total_groups
340
344
  FROM ${schemaPrefix}"${table}"
341
345
  ${whereClause}
342
346
  `;
343
- const groupCountResult = await adapter.executeQuery(groupCountSql);
344
- const totalGroupCount = Number(groupCountResult.rows?.[0]
345
- ?.total_groups ?? 0);
346
- // Grouped time series
347
- const sql = `
347
+ const groupCountResult = await adapter.executeQuery(groupCountSql);
348
+ const totalGroupCount = Number(groupCountResult.rows?.[0]
349
+ ?.total_groups ?? 0);
350
+ // Grouped time series
351
+ const sql = `
348
352
  SELECT
349
353
  "${groupBy}" as group_key,
350
354
  DATE_TRUNC('${interval}', "${timeColumn}") as time_bucket,
@@ -355,77 +359,78 @@ export function createStatsTimeSeriesTool(adapter) {
355
359
  GROUP BY "${groupBy}", DATE_TRUNC('${interval}', "${timeColumn}")
356
360
  ORDER BY "${groupBy}", time_bucket DESC
357
361
  `;
358
- const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
359
- ? [queryParams]
360
- : []));
361
- const rows = result.rows ?? [];
362
- // Group results by group_key
363
- const groupsMap = new Map();
364
- const groupsTotalCount = new Map();
365
- let groupsProcessed = 0;
366
- for (const row of rows) {
367
- const key = row["group_key"];
368
- if (!groupsMap.has(key)) {
369
- // Check if we've hit the group limit
370
- if (effectiveGroupLimit !== undefined &&
371
- groupsProcessed >= effectiveGroupLimit) {
372
- continue;
362
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
363
+ ? [queryParams]
364
+ : []));
365
+ const rows = result.rows ?? [];
366
+ // Group results by group_key
367
+ const groupsMap = new Map();
368
+ const groupsTotalCount = new Map();
369
+ let groupsProcessed = 0;
370
+ for (const row of rows) {
371
+ const key = row["group_key"];
372
+ if (!groupsMap.has(key)) {
373
+ // Check if we've hit the group limit
374
+ if (effectiveGroupLimit !== undefined &&
375
+ groupsProcessed >= effectiveGroupLimit) {
376
+ continue;
377
+ }
378
+ groupsMap.set(key, []);
379
+ groupsTotalCount.set(key, 0);
380
+ groupsProcessed++;
381
+ }
382
+ const currentTotal = groupsTotalCount.get(key) ?? 0;
383
+ groupsTotalCount.set(key, currentTotal + 1);
384
+ const bucketList = groupsMap.get(key);
385
+ // Only add if no limit or under limit
386
+ if (bucketList !== undefined &&
387
+ (effectiveLimit === undefined ||
388
+ bucketList.length < effectiveLimit)) {
389
+ bucketList.push(mapBucket(row));
373
390
  }
374
- groupsMap.set(key, []);
375
- groupsTotalCount.set(key, 0);
376
- groupsProcessed++;
377
391
  }
378
- const currentTotal = groupsTotalCount.get(key) ?? 0;
379
- groupsTotalCount.set(key, currentTotal + 1);
380
- const bucketList = groupsMap.get(key);
381
- // Only add if no limit or under limit
382
- if (bucketList !== undefined &&
383
- (effectiveLimit === undefined || bucketList.length < effectiveLimit)) {
384
- bucketList.push(mapBucket(row));
392
+ const groups = Array.from(groupsMap.entries()).map(([key, buckets]) => ({
393
+ groupKey: key,
394
+ buckets,
395
+ }));
396
+ // Build response with truncation indicators
397
+ const response = {
398
+ table: `${schema ?? "public"}.${table}`,
399
+ valueColumn,
400
+ timeColumn,
401
+ interval,
402
+ aggregation: agg,
403
+ groupBy,
404
+ groups,
405
+ count: groups.length,
406
+ };
407
+ // Add truncation indicators when groups are limited
408
+ const groupsTruncated = effectiveGroupLimit !== undefined &&
409
+ totalGroupCount > effectiveGroupLimit;
410
+ if (groupsTruncated || !userProvidedGroupLimit) {
411
+ response["truncated"] = groupsTruncated;
412
+ response["totalGroupCount"] = totalGroupCount;
385
413
  }
414
+ return response;
386
415
  }
387
- const groups = Array.from(groupsMap.entries()).map(([key, buckets]) => ({
388
- groupKey: key,
389
- buckets,
390
- }));
391
- // Build response with truncation indicators
392
- const response = {
393
- table: `${schema ?? "public"}.${table}`,
394
- valueColumn,
395
- timeColumn,
396
- interval,
397
- aggregation: agg,
398
- groupBy,
399
- groups,
400
- count: groups.length,
401
- };
402
- // Add truncation indicators when groups are limited
403
- const groupsTruncated = effectiveGroupLimit !== undefined &&
404
- totalGroupCount > effectiveGroupLimit;
405
- if (groupsTruncated || !userProvidedGroupLimit) {
406
- response["truncated"] = groupsTruncated;
407
- response["totalGroupCount"] = totalGroupCount;
408
- }
409
- return response;
410
- }
411
- // Ungrouped time series
412
- // Build LIMIT clause: no LIMIT if effectiveLimit is undefined (limit: 0)
413
- const limitClause = effectiveLimit !== undefined ? `LIMIT ${String(effectiveLimit)}` : "";
414
- // Get total count if using default limit (for truncation indicator)
415
- let totalCount;
416
- if (usingDefaultLimit) {
417
- const countSql = `
416
+ // Ungrouped time series
417
+ // Build LIMIT clause: no LIMIT if effectiveLimit is undefined (limit: 0)
418
+ const limitClause = effectiveLimit !== undefined ? `LIMIT ${String(effectiveLimit)}` : "";
419
+ // Get total count if using default limit (for truncation indicator)
420
+ let totalCount;
421
+ if (usingDefaultLimit) {
422
+ const countSql = `
418
423
  SELECT COUNT(DISTINCT DATE_TRUNC('${interval}', "${timeColumn}")) as total_buckets
419
424
  FROM ${schemaPrefix}"${table}"
420
425
  ${whereClause}
421
426
  `;
422
- const countResult = await adapter.executeQuery(countSql, ...(queryParams !== undefined && queryParams.length > 0
423
- ? [queryParams]
424
- : []));
425
- const countRow = countResult.rows?.[0];
426
- totalCount = countRow ? Number(countRow.total_buckets) : undefined;
427
- }
428
- const sql = `
427
+ const countResult = await adapter.executeQuery(countSql, ...(queryParams !== undefined && queryParams.length > 0
428
+ ? [queryParams]
429
+ : []));
430
+ const countRow = countResult.rows?.[0];
431
+ totalCount = countRow ? Number(countRow.total_buckets) : undefined;
432
+ }
433
+ const sql = `
429
434
  SELECT
430
435
  DATE_TRUNC('${interval}', "${timeColumn}") as time_bucket,
431
436
  ${agg.toUpperCase()}("${valueColumn}")::numeric(20,6) as value,
@@ -436,25 +441,32 @@ export function createStatsTimeSeriesTool(adapter) {
436
441
  ORDER BY time_bucket DESC
437
442
  ${limitClause}
438
443
  `;
439
- const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
440
- ? [queryParams]
441
- : []));
442
- const buckets = (result.rows ?? []).map((row) => mapBucket(row));
443
- // Build response
444
- const response = {
445
- table: `${schema ?? "public"}.${table}`,
446
- valueColumn,
447
- timeColumn,
448
- interval,
449
- aggregation: agg,
450
- buckets,
451
- };
452
- // Add truncation indicators when default limit was applied
453
- if (usingDefaultLimit && totalCount !== undefined) {
454
- response["truncated"] = buckets.length < totalCount;
455
- response["totalCount"] = totalCount;
444
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
445
+ ? [queryParams]
446
+ : []));
447
+ const buckets = (result.rows ?? []).map((row) => mapBucket(row));
448
+ // Build response
449
+ const response = {
450
+ table: `${schema ?? "public"}.${table}`,
451
+ valueColumn,
452
+ timeColumn,
453
+ interval,
454
+ aggregation: agg,
455
+ buckets,
456
+ };
457
+ // Add truncation indicators when default limit was applied
458
+ if (usingDefaultLimit && totalCount !== undefined) {
459
+ response["truncated"] = buckets.length < totalCount;
460
+ response["totalCount"] = totalCount;
461
+ }
462
+ return response;
463
+ }
464
+ catch (error) {
465
+ return {
466
+ success: false,
467
+ error: formatPostgresError(error, { tool: "pg_stats_time_series" }),
468
+ };
456
469
  }
457
- return response;
458
470
  },
459
471
  };
460
472
  }
@@ -471,22 +483,23 @@ export function createStatsDistributionTool(adapter) {
471
483
  annotations: readOnly("Distribution Analysis"),
472
484
  icons: getToolIcons("stats", readOnly("Distribution Analysis")),
473
485
  handler: async (params, _context) => {
474
- const parsed = StatsDistributionSchema.parse(params);
475
- const { table, column, buckets, schema, where, params: queryParams, groupBy, groupLimit, } = parsed;
476
- const schemaName = schema ?? "public";
477
- const schemaPrefix = schema ? `"${schema}".` : "";
478
- const whereClause = where ? `WHERE ${where}` : "";
479
- const numBuckets = buckets ?? 10;
480
- // Validate column exists and is numeric
481
- await validateNumericColumn(adapter, table, column, schemaName);
482
- // Helper to compute skewness and kurtosis for a given group
483
- const computeMoments = async (groupFilter) => {
484
- const filterClause = groupFilter
485
- ? whereClause
486
- ? `${whereClause} AND ${groupFilter}`
487
- : `WHERE ${groupFilter}`
488
- : whereClause;
489
- const statsQuery = `
486
+ try {
487
+ const parsed = StatsDistributionSchema.parse(params);
488
+ const { table, column, buckets, schema, where, params: queryParams, groupBy, groupLimit, } = parsed;
489
+ const schemaName = schema ?? "public";
490
+ const schemaPrefix = schema ? `"${schema}".` : "";
491
+ const whereClause = where ? `WHERE ${where}` : "";
492
+ const numBuckets = buckets ?? 10;
493
+ // Validate column exists and is numeric
494
+ await validateNumericColumn(adapter, table, column, schemaName);
495
+ // Helper to compute skewness and kurtosis for a given group
496
+ const computeMoments = async (groupFilter) => {
497
+ const filterClause = groupFilter
498
+ ? whereClause
499
+ ? `${whereClause} AND ${groupFilter}`
500
+ : `WHERE ${groupFilter}`
501
+ : whereClause;
502
+ const statsQuery = `
490
503
  WITH stats AS (
491
504
  SELECT
492
505
  MIN("${column}") as min_val,
@@ -516,28 +529,28 @@ export function createStatsDistributionTool(adapter) {
516
529
  )
517
530
  SELECT * FROM moments
518
531
  `;
519
- const result = await adapter.executeQuery(statsQuery, ...(queryParams !== undefined && queryParams.length > 0
520
- ? [queryParams]
521
- : []));
522
- const row = result.rows?.[0];
523
- if (row?.["min_val"] == null || row["max_val"] == null) {
524
- return null;
525
- }
526
- return {
527
- minVal: Number(row["min_val"]),
528
- maxVal: Number(row["max_val"]),
529
- skewness: row["skewness"] !== null ? Number(row["skewness"]) : null,
530
- kurtosis: row["kurtosis"] !== null ? Number(row["kurtosis"]) : null,
532
+ const result = await adapter.executeQuery(statsQuery, ...(queryParams !== undefined && queryParams.length > 0
533
+ ? [queryParams]
534
+ : []));
535
+ const row = result.rows?.[0];
536
+ if (row?.["min_val"] == null || row["max_val"] == null) {
537
+ return null;
538
+ }
539
+ return {
540
+ minVal: Number(row["min_val"]),
541
+ maxVal: Number(row["max_val"]),
542
+ skewness: row["skewness"] !== null ? Number(row["skewness"]) : null,
543
+ kurtosis: row["kurtosis"] !== null ? Number(row["kurtosis"]) : null,
544
+ };
531
545
  };
532
- };
533
- // Helper to generate histogram for given min/max
534
- const generateHistogram = async (minVal, maxVal, groupFilter) => {
535
- const filterClause = groupFilter
536
- ? whereClause
537
- ? `${whereClause} AND ${groupFilter}`
538
- : `WHERE ${groupFilter}`
539
- : whereClause;
540
- const histogramQuery = `
546
+ // Helper to generate histogram for given min/max
547
+ const generateHistogram = async (minVal, maxVal, groupFilter) => {
548
+ const filterClause = groupFilter
549
+ ? whereClause
550
+ ? `${whereClause} AND ${groupFilter}`
551
+ : `WHERE ${groupFilter}`
552
+ : whereClause;
553
+ const histogramQuery = `
541
554
  SELECT
542
555
  WIDTH_BUCKET("${column}", ${String(minVal)}, ${String(maxVal + 0.0001)}, ${String(numBuckets)}) as bucket,
543
556
  COUNT(*) as frequency,
@@ -548,92 +561,99 @@ export function createStatsDistributionTool(adapter) {
548
561
  GROUP BY WIDTH_BUCKET("${column}", ${String(minVal)}, ${String(maxVal + 0.0001)}, ${String(numBuckets)})
549
562
  ORDER BY bucket
550
563
  `;
551
- const result = await adapter.executeQuery(histogramQuery, ...(queryParams !== undefined && queryParams.length > 0
552
- ? [queryParams]
553
- : []));
554
- return (result.rows ?? []).map((row) => ({
555
- bucket: Number(row["bucket"]),
556
- frequency: Number(row["frequency"]),
557
- rangeMin: Number(row["bucket_min"]),
558
- rangeMax: Number(row["bucket_max"]),
559
- }));
560
- };
561
- if (groupBy !== undefined) {
562
- // Handle groupLimit: undefined uses default (20), 0 means no limit
563
- const DEFAULT_GROUP_LIMIT = 20;
564
- const userProvidedGroupLimit = groupLimit !== undefined;
565
- const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
566
- // Get distinct groups first
567
- const groupsQuery = `
564
+ const result = await adapter.executeQuery(histogramQuery, ...(queryParams !== undefined && queryParams.length > 0
565
+ ? [queryParams]
566
+ : []));
567
+ return (result.rows ?? []).map((row) => ({
568
+ bucket: Number(row["bucket"]),
569
+ frequency: Number(row["frequency"]),
570
+ rangeMin: Number(row["bucket_min"]),
571
+ rangeMax: Number(row["bucket_max"]),
572
+ }));
573
+ };
574
+ if (groupBy !== undefined) {
575
+ // Handle groupLimit: undefined uses default (20), 0 means no limit
576
+ const DEFAULT_GROUP_LIMIT = 20;
577
+ const userProvidedGroupLimit = groupLimit !== undefined;
578
+ const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
579
+ // Get distinct groups first
580
+ const groupsQuery = `
568
581
  SELECT DISTINCT "${groupBy}" as group_key
569
582
  FROM ${schemaPrefix}"${table}"
570
583
  ${whereClause}
571
584
  ORDER BY "${groupBy}"
572
585
  `;
573
- const groupsResult = await adapter.executeQuery(groupsQuery, ...(queryParams !== undefined && queryParams.length > 0
574
- ? [queryParams]
575
- : []));
576
- const allGroupKeys = (groupsResult.rows ?? []).map((r) => r["group_key"]);
577
- const totalGroupCount = allGroupKeys.length;
578
- // Apply group limit
579
- const groupKeys = effectiveGroupLimit !== undefined
580
- ? allGroupKeys.slice(0, effectiveGroupLimit)
581
- : allGroupKeys;
582
- // Process each group
583
- const groups = [];
584
- for (const groupKey of groupKeys) {
585
- const groupFilter = typeof groupKey === "string"
586
- ? `"${groupBy}" = '${groupKey.replace(/'/g, "''")}'`
587
- : `"${groupBy}" = ${String(groupKey)}`;
588
- const moments = await computeMoments(groupFilter);
589
- if (moments === null)
590
- continue;
591
- const { minVal, maxVal, skewness, kurtosis } = moments;
592
- const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
593
- const histogram = await generateHistogram(minVal, maxVal, groupFilter);
594
- groups.push({
595
- groupKey,
596
- range: { min: minVal, max: maxVal },
597
- bucketWidth,
598
- skewness,
599
- kurtosis,
600
- histogram,
601
- });
586
+ const groupsResult = await adapter.executeQuery(groupsQuery, ...(queryParams !== undefined && queryParams.length > 0
587
+ ? [queryParams]
588
+ : []));
589
+ const allGroupKeys = (groupsResult.rows ?? []).map((r) => r["group_key"]);
590
+ const totalGroupCount = allGroupKeys.length;
591
+ // Apply group limit
592
+ const groupKeys = effectiveGroupLimit !== undefined
593
+ ? allGroupKeys.slice(0, effectiveGroupLimit)
594
+ : allGroupKeys;
595
+ // Process each group
596
+ const groups = [];
597
+ for (const groupKey of groupKeys) {
598
+ const groupFilter = typeof groupKey === "string"
599
+ ? `"${groupBy}" = '${groupKey.replace(/'/g, "''")}'`
600
+ : `"${groupBy}" = ${String(groupKey)}`;
601
+ const moments = await computeMoments(groupFilter);
602
+ if (moments === null)
603
+ continue;
604
+ const { minVal, maxVal, skewness, kurtosis } = moments;
605
+ const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
606
+ const histogram = await generateHistogram(minVal, maxVal, groupFilter);
607
+ groups.push({
608
+ groupKey,
609
+ range: { min: minVal, max: maxVal },
610
+ bucketWidth,
611
+ skewness,
612
+ kurtosis,
613
+ histogram,
614
+ });
615
+ }
616
+ // Build response with truncation indicators
617
+ const response = {
618
+ table: `${schema ?? "public"}.${table}`,
619
+ column,
620
+ groupBy,
621
+ groups,
622
+ count: groups.length,
623
+ };
624
+ // Add truncation indicators when groups are limited
625
+ const groupsTruncated = effectiveGroupLimit !== undefined &&
626
+ totalGroupCount > effectiveGroupLimit;
627
+ if (groupsTruncated || !userProvidedGroupLimit) {
628
+ response["truncated"] = groupsTruncated;
629
+ response["totalGroupCount"] = totalGroupCount;
630
+ }
631
+ return response;
602
632
  }
603
- // Build response with truncation indicators
604
- const response = {
633
+ // Ungrouped distribution (existing logic)
634
+ const moments = await computeMoments();
635
+ if (moments === null) {
636
+ return { error: "No data or all nulls in column" };
637
+ }
638
+ const { minVal, maxVal, skewness, kurtosis } = moments;
639
+ const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
640
+ const histogram = await generateHistogram(minVal, maxVal);
641
+ return {
605
642
  table: `${schema ?? "public"}.${table}`,
606
643
  column,
607
- groupBy,
608
- groups,
609
- count: groups.length,
644
+ range: { min: minVal, max: maxVal },
645
+ bucketWidth,
646
+ skewness,
647
+ kurtosis,
648
+ histogram,
610
649
  };
611
- // Add truncation indicators when groups are limited
612
- const groupsTruncated = effectiveGroupLimit !== undefined &&
613
- totalGroupCount > effectiveGroupLimit;
614
- if (groupsTruncated || !userProvidedGroupLimit) {
615
- response["truncated"] = groupsTruncated;
616
- response["totalGroupCount"] = totalGroupCount;
617
- }
618
- return response;
619
650
  }
620
- // Ungrouped distribution (existing logic)
621
- const moments = await computeMoments();
622
- if (moments === null) {
623
- return { error: "No data or all nulls in column" };
651
+ catch (error) {
652
+ return {
653
+ success: false,
654
+ error: formatPostgresError(error, { tool: "pg_stats_distribution" }),
655
+ };
624
656
  }
625
- const { minVal, maxVal, skewness, kurtosis } = moments;
626
- const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
627
- const histogram = await generateHistogram(minVal, maxVal);
628
- return {
629
- table: `${schema ?? "public"}.${table}`,
630
- column,
631
- range: { min: minVal, max: maxVal },
632
- bucketWidth,
633
- skewness,
634
- kurtosis,
635
- histogram,
636
- };
637
657
  },
638
658
  };
639
659
  }
@@ -650,87 +670,91 @@ export function createStatsHypothesisTool(adapter) {
650
670
  annotations: readOnly("Hypothesis Testing"),
651
671
  icons: getToolIcons("stats", readOnly("Hypothesis Testing")),
652
672
  handler: async (params, _context) => {
653
- const { table, column, testType, hypothesizedMean, populationStdDev, schema, where, params: queryParams, groupBy, } = StatsHypothesisSchema.parse(params);
654
- const schemaName = schema ?? "public";
655
- const schemaPrefix = schema ? `"${schema}".` : "";
656
- const whereClause = where ? `WHERE ${where}` : "";
657
- // Validate column exists and is numeric
658
- await validateNumericColumn(adapter, table, column, schemaName);
659
- // Helper to calculate test results from row stats
660
- const calculateTestResults = (n, sampleMean, sampleStdDev) => {
661
- if (n < 2 || isNaN(sampleStdDev) || sampleStdDev === 0) {
662
- return { error: "Insufficient data or zero variance", sampleSize: n };
663
- }
664
- let stddevUsed;
665
- let stddevNote;
666
- if (testType === "z_test") {
667
- if (populationStdDev !== undefined) {
668
- stddevUsed = populationStdDev;
673
+ try {
674
+ const { table, column, testType, hypothesizedMean, populationStdDev, schema, where, params: queryParams, groupBy, } = StatsHypothesisSchema.parse(params);
675
+ const schemaName = schema ?? "public";
676
+ const schemaPrefix = schema ? `"${schema}".` : "";
677
+ const whereClause = where ? `WHERE ${where}` : "";
678
+ // Validate column exists and is numeric
679
+ await validateNumericColumn(adapter, table, column, schemaName);
680
+ // Helper to calculate test results from row stats
681
+ const calculateTestResults = (n, sampleMean, sampleStdDev) => {
682
+ if (n < 2 || isNaN(sampleStdDev) || sampleStdDev === 0) {
683
+ return {
684
+ error: "Insufficient data or zero variance",
685
+ sampleSize: n,
686
+ };
687
+ }
688
+ let stddevUsed;
689
+ let stddevNote;
690
+ if (testType === "z_test") {
691
+ if (populationStdDev !== undefined) {
692
+ stddevUsed = populationStdDev;
693
+ }
694
+ else {
695
+ stddevUsed = sampleStdDev;
696
+ stddevNote =
697
+ "No populationStdDev provided; using sample stddev (less accurate for z-test)";
698
+ }
669
699
  }
670
700
  else {
671
701
  stddevUsed = sampleStdDev;
672
- stddevNote =
673
- "No populationStdDev provided; using sample stddev (less accurate for z-test)";
674
702
  }
675
- }
676
- else {
677
- stddevUsed = sampleStdDev;
678
- }
679
- const standardError = stddevUsed / Math.sqrt(n);
680
- const testStatistic = (sampleMean - hypothesizedMean) / standardError;
681
- const degreesOfFreedom = n - 1;
682
- // Calculate p-value based on test type
683
- const pValue = testType === "z_test"
684
- ? calculateZTestPValue(testStatistic)
685
- : calculateTTestPValue(testStatistic, degreesOfFreedom);
686
- // Round p-value to 6 decimal places for cleaner output
687
- const pValueRounded = Math.round(pValue * 1e6) / 1e6;
688
- // Determine significance based on p-value
689
- let interpretation;
690
- if (pValueRounded < 0.001) {
691
- interpretation =
692
- "Highly significant (p < 0.001): Strong evidence against the null hypothesis";
693
- }
694
- else if (pValueRounded < 0.01) {
695
- interpretation =
696
- "Very significant (p < 0.01): Strong evidence against the null hypothesis";
697
- }
698
- else if (pValueRounded < 0.05) {
699
- interpretation =
700
- "Significant (p < 0.05): Evidence against the null hypothesis at α=0.05 level";
701
- }
702
- else if (pValueRounded < 0.1) {
703
- interpretation =
704
- "Marginally significant (p < 0.1): Weak evidence against the null hypothesis";
705
- }
706
- else {
707
- interpretation =
708
- "Not significant (p 0.1): Insufficient evidence to reject the null hypothesis";
709
- }
710
- // Build note with warnings
711
- let noteText = stddevNote ??
712
- "Two-tailed p-value calculated using numerical approximation";
713
- if (n < 30) {
714
- noteText =
715
- `Small sample size (n=${String(n)}): results may be less reliable. ` +
716
- noteText;
717
- }
718
- return {
719
- sampleSize: n,
720
- sampleMean,
721
- sampleStdDev,
722
- populationStdDev: testType === "z_test" ? (populationStdDev ?? null) : null,
723
- standardError,
724
- testStatistic,
725
- pValue: pValueRounded,
726
- degreesOfFreedom: testType === "t_test" ? degreesOfFreedom : null,
727
- interpretation,
728
- note: noteText,
703
+ const standardError = stddevUsed / Math.sqrt(n);
704
+ const testStatistic = (sampleMean - hypothesizedMean) / standardError;
705
+ const degreesOfFreedom = n - 1;
706
+ // Calculate p-value based on test type
707
+ const pValue = testType === "z_test"
708
+ ? calculateZTestPValue(testStatistic)
709
+ : calculateTTestPValue(testStatistic, degreesOfFreedom);
710
+ // Round p-value to 6 decimal places for cleaner output
711
+ const pValueRounded = Math.round(pValue * 1e6) / 1e6;
712
+ // Determine significance based on p-value
713
+ let interpretation;
714
+ if (pValueRounded < 0.001) {
715
+ interpretation =
716
+ "Highly significant (p < 0.001): Strong evidence against the null hypothesis";
717
+ }
718
+ else if (pValueRounded < 0.01) {
719
+ interpretation =
720
+ "Very significant (p < 0.01): Strong evidence against the null hypothesis";
721
+ }
722
+ else if (pValueRounded < 0.05) {
723
+ interpretation =
724
+ "Significant (p < 0.05): Evidence against the null hypothesis at α=0.05 level";
725
+ }
726
+ else if (pValueRounded < 0.1) {
727
+ interpretation =
728
+ "Marginally significant (p < 0.1): Weak evidence against the null hypothesis";
729
+ }
730
+ else {
731
+ interpretation =
732
+ "Not significant (p 0.1): Insufficient evidence to reject the null hypothesis";
733
+ }
734
+ // Build note with warnings
735
+ let noteText = stddevNote ??
736
+ "Two-tailed p-value calculated using numerical approximation";
737
+ if (n < 30) {
738
+ noteText =
739
+ `Small sample size (n=${String(n)}): results may be less reliable. ` +
740
+ noteText;
741
+ }
742
+ return {
743
+ sampleSize: n,
744
+ sampleMean,
745
+ sampleStdDev,
746
+ populationStdDev: testType === "z_test" ? (populationStdDev ?? null) : null,
747
+ standardError,
748
+ testStatistic,
749
+ pValue: pValueRounded,
750
+ degreesOfFreedom: testType === "t_test" ? degreesOfFreedom : null,
751
+ interpretation,
752
+ note: noteText,
753
+ };
729
754
  };
730
- };
731
- if (groupBy !== undefined) {
732
- // Grouped hypothesis tests
733
- const sql = `
755
+ if (groupBy !== undefined) {
756
+ // Grouped hypothesis tests
757
+ const sql = `
734
758
  SELECT
735
759
  "${groupBy}" as group_key,
736
760
  COUNT("${column}") as n,
@@ -741,59 +765,66 @@ export function createStatsHypothesisTool(adapter) {
741
765
  GROUP BY "${groupBy}"
742
766
  ORDER BY "${groupBy}"
743
767
  `;
768
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
769
+ ? [queryParams]
770
+ : []));
771
+ const rows = result.rows ?? [];
772
+ const groups = rows.map((row) => {
773
+ const n = Number(row["n"]);
774
+ const sampleMean = Number(row["mean"]);
775
+ const sampleStdDev = Number(row["stddev"]);
776
+ return {
777
+ groupKey: row["group_key"],
778
+ results: calculateTestResults(n, sampleMean, sampleStdDev),
779
+ };
780
+ });
781
+ return {
782
+ table: `${schema ?? "public"}.${table}`,
783
+ column,
784
+ testType,
785
+ hypothesizedMean,
786
+ groupBy,
787
+ groups,
788
+ count: groups.length,
789
+ };
790
+ }
791
+ // Ungrouped hypothesis test
792
+ const sql = `
793
+ SELECT
794
+ COUNT("${column}") as n,
795
+ AVG("${column}")::numeric(20,6) as mean,
796
+ STDDEV_SAMP("${column}")::numeric(20,6) as stddev
797
+ FROM ${schemaPrefix}"${table}"
798
+ ${whereClause}
799
+ `;
744
800
  const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
745
801
  ? [queryParams]
746
802
  : []));
747
- const rows = result.rows ?? [];
748
- const groups = rows.map((row) => {
749
- const n = Number(row["n"]);
750
- const sampleMean = Number(row["mean"]);
751
- const sampleStdDev = Number(row["stddev"]);
752
- return {
753
- groupKey: row["group_key"],
754
- results: calculateTestResults(n, sampleMean, sampleStdDev),
755
- };
756
- });
803
+ const row = result.rows?.[0];
804
+ if (!row)
805
+ return { error: "No data found" };
806
+ const n = Number(row.n);
807
+ const sampleMean = Number(row.mean);
808
+ const sampleStdDev = Number(row.stddev);
809
+ const testResults = calculateTestResults(n, sampleMean, sampleStdDev);
810
+ // If error, return at top level (not nested in results)
811
+ if ("error" in testResults) {
812
+ return testResults;
813
+ }
757
814
  return {
758
815
  table: `${schema ?? "public"}.${table}`,
759
816
  column,
760
817
  testType,
761
818
  hypothesizedMean,
762
- groupBy,
763
- groups,
764
- count: groups.length,
819
+ results: testResults,
765
820
  };
766
821
  }
767
- // Ungrouped hypothesis test
768
- const sql = `
769
- SELECT
770
- COUNT("${column}") as n,
771
- AVG("${column}")::numeric(20,6) as mean,
772
- STDDEV_SAMP("${column}")::numeric(20,6) as stddev
773
- FROM ${schemaPrefix}"${table}"
774
- ${whereClause}
775
- `;
776
- const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
777
- ? [queryParams]
778
- : []));
779
- const row = result.rows?.[0];
780
- if (!row)
781
- return { error: "No data found" };
782
- const n = Number(row.n);
783
- const sampleMean = Number(row.mean);
784
- const sampleStdDev = Number(row.stddev);
785
- const testResults = calculateTestResults(n, sampleMean, sampleStdDev);
786
- // If error, return at top level (not nested in results)
787
- if ("error" in testResults) {
788
- return testResults;
822
+ catch (error) {
823
+ return {
824
+ success: false,
825
+ error: formatPostgresError(error, { tool: "pg_stats_hypothesis" }),
826
+ };
789
827
  }
790
- return {
791
- table: `${schema ?? "public"}.${table}`,
792
- column,
793
- testType,
794
- hypothesizedMean,
795
- results: testResults,
796
- };
797
828
  },
798
829
  };
799
830
  }
@@ -810,105 +841,113 @@ export function createStatsSamplingTool(adapter) {
810
841
  annotations: readOnly("Random Sampling"),
811
842
  icons: getToolIcons("stats", readOnly("Random Sampling")),
812
843
  handler: async (params, _context) => {
813
- const { table, method, sampleSize, percentage, schema, select, where, params: queryParams, } = StatsSamplingSchema.parse(params);
814
- const schemaName = schema ?? "public";
815
- // Validate table exists
816
- await validateTableExists(adapter, table, schemaName);
817
- const schemaPrefix = schema ? `"${schema}".` : "";
818
- const columns = select && select.length > 0
819
- ? select.map((c) => `"${c}"`).join(", ")
820
- : "*";
821
- const whereClause = where ? `WHERE ${where}` : "";
822
- const samplingMethod = method ?? "random";
823
- let sql;
824
- let note;
825
- // If sampleSize is provided, always use ORDER BY RANDOM() LIMIT n for exact counts
826
- // TABLESAMPLE BERNOULLI/SYSTEM are percentage-based and cannot guarantee exact row counts
827
- if (sampleSize !== undefined) {
828
- const limit = sampleSize;
829
- sql = `
844
+ try {
845
+ const { table, method, sampleSize, percentage, schema, select, where, params: queryParams, } = StatsSamplingSchema.parse(params);
846
+ const schemaName = schema ?? "public";
847
+ // Validate table exists
848
+ await validateTableExists(adapter, table, schemaName);
849
+ const schemaPrefix = schema ? `"${schema}".` : "";
850
+ const columns = select && select.length > 0
851
+ ? select.map((c) => `"${c}"`).join(", ")
852
+ : "*";
853
+ const whereClause = where ? `WHERE ${where}` : "";
854
+ const samplingMethod = method ?? "random";
855
+ let sql;
856
+ let note;
857
+ // If sampleSize is provided, always use ORDER BY RANDOM() LIMIT n for exact counts
858
+ // TABLESAMPLE BERNOULLI/SYSTEM are percentage-based and cannot guarantee exact row counts
859
+ if (sampleSize !== undefined) {
860
+ const limit = sampleSize;
861
+ sql = `
830
862
  SELECT ${columns}
831
863
  FROM ${schemaPrefix}"${table}"
832
864
  ${whereClause}
833
865
  ORDER BY RANDOM()
834
866
  LIMIT ${String(limit)}
835
867
  `;
836
- if (percentage !== undefined) {
837
- note = `sampleSize (${String(sampleSize)}) takes precedence over percentage (${String(percentage)}%). Using ORDER BY RANDOM() LIMIT for exact row count.`;
838
- }
839
- else if (samplingMethod !== "random") {
840
- note = `Using ORDER BY RANDOM() LIMIT for exact ${String(sampleSize)} row count. TABLESAMPLE ${samplingMethod.toUpperCase()} is percentage-based and cannot guarantee exact counts.`;
868
+ if (percentage !== undefined) {
869
+ note = `sampleSize (${String(sampleSize)}) takes precedence over percentage (${String(percentage)}%). Using ORDER BY RANDOM() LIMIT for exact row count.`;
870
+ }
871
+ else if (samplingMethod !== "random") {
872
+ note = `Using ORDER BY RANDOM() LIMIT for exact ${String(sampleSize)} row count. TABLESAMPLE ${samplingMethod.toUpperCase()} is percentage-based and cannot guarantee exact counts.`;
873
+ }
841
874
  }
842
- }
843
- else if (samplingMethod === "random") {
844
- // Default random sampling with default limit (20 to reduce LLM context usage)
845
- const limit = 20;
846
- sql = `
875
+ else if (samplingMethod === "random") {
876
+ // Default random sampling with default limit (20 to reduce LLM context usage)
877
+ const limit = 20;
878
+ sql = `
847
879
  SELECT ${columns}
848
880
  FROM ${schemaPrefix}"${table}"
849
881
  ${whereClause}
850
882
  ORDER BY RANDOM()
851
883
  LIMIT ${String(limit)}
852
884
  `;
853
- if (percentage !== undefined) {
854
- note = `percentage (${String(percentage)}%) is ignored for random method. Use method:'bernoulli' or method:'system' for percentage-based sampling, or use sampleSize for exact row count.`;
885
+ if (percentage !== undefined) {
886
+ note = `percentage (${String(percentage)}%) is ignored for random method. Use method:'bernoulli' or method:'system' for percentage-based sampling, or use sampleSize for exact row count.`;
887
+ }
855
888
  }
856
- }
857
- else {
858
- // TABLESAMPLE with percentage (approximate row count)
859
- // Apply default limit to prevent large payloads
860
- const pct = percentage ?? 10;
861
- const DEFAULT_TABLESAMPLE_LIMIT = 100;
862
- sql = `
889
+ else {
890
+ // TABLESAMPLE with percentage (approximate row count)
891
+ // Apply default limit to prevent large payloads
892
+ const pct = percentage ?? 10;
893
+ const DEFAULT_TABLESAMPLE_LIMIT = 100;
894
+ sql = `
863
895
  SELECT ${columns}
864
896
  FROM ${schemaPrefix}"${table}"
865
897
  TABLESAMPLE ${samplingMethod.toUpperCase()}(${String(pct)})
866
898
  ${whereClause}
867
899
  LIMIT ${String(DEFAULT_TABLESAMPLE_LIMIT + 1)}
868
900
  `;
869
- // Add hint about system method unreliability for small tables
870
- const methodHint = samplingMethod === "system"
871
- ? " Consider using 'bernoulli' or 'random' method for more reliable results on small tables."
872
- : "";
873
- note = `TABLESAMPLE ${samplingMethod.toUpperCase()}(${String(pct)}%) returns approximately ${String(pct)}% of rows. Actual count varies based on table size and sampling algorithm.${methodHint}`;
874
- }
875
- const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
876
- ? [queryParams]
877
- : []));
878
- let rows = result.rows ?? [];
879
- // Check if we need to truncate due to default limit for TABLESAMPLE methods
880
- let truncated = false;
881
- let totalSampled;
882
- const DEFAULT_TABLESAMPLE_LIMIT = 100;
883
- if (sampleSize === undefined &&
884
- samplingMethod !== "random" &&
885
- rows.length > DEFAULT_TABLESAMPLE_LIMIT) {
886
- totalSampled = rows.length;
887
- rows = rows.slice(0, DEFAULT_TABLESAMPLE_LIMIT);
888
- truncated = true;
889
- }
890
- const response = {
891
- table: `${schema ?? "public"}.${table}`,
892
- method: samplingMethod,
893
- sampleSize: rows.length,
894
- rows,
895
- };
896
- // Add truncation indicators if applicable
897
- if (truncated && totalSampled !== undefined) {
898
- response.truncated = truncated;
899
- response.totalSampled = totalSampled;
900
- }
901
- if (note !== undefined) {
902
- response.note = note;
901
+ // Add hint about system method unreliability for small tables
902
+ const methodHint = samplingMethod === "system"
903
+ ? " Consider using 'bernoulli' or 'random' method for more reliable results on small tables."
904
+ : "";
905
+ note = `TABLESAMPLE ${samplingMethod.toUpperCase()}(${String(pct)}%) returns approximately ${String(pct)}% of rows. Actual count varies based on table size and sampling algorithm.${methodHint}`;
906
+ }
907
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
908
+ ? [queryParams]
909
+ : []));
910
+ let rows = result.rows ?? [];
911
+ // Check if we need to truncate due to default limit for TABLESAMPLE methods
912
+ let truncated = false;
913
+ let totalSampled;
914
+ const DEFAULT_TABLESAMPLE_LIMIT = 100;
915
+ if (sampleSize === undefined &&
916
+ samplingMethod !== "random" &&
917
+ rows.length > DEFAULT_TABLESAMPLE_LIMIT) {
918
+ totalSampled = rows.length;
919
+ rows = rows.slice(0, DEFAULT_TABLESAMPLE_LIMIT);
920
+ truncated = true;
921
+ }
922
+ const response = {
923
+ table: `${schema ?? "public"}.${table}`,
924
+ method: samplingMethod,
925
+ sampleSize: rows.length,
926
+ rows,
927
+ };
928
+ // Add truncation indicators if applicable
929
+ if (truncated && totalSampled !== undefined) {
930
+ response.truncated = truncated;
931
+ response.totalSampled = totalSampled;
932
+ }
933
+ if (note !== undefined) {
934
+ response.note = note;
935
+ }
936
+ // Add note if requested sampleSize exceeded available rows
937
+ if (sampleSize !== undefined && rows.length < sampleSize) {
938
+ const existingNote = response.note !== undefined ? response.note + " " : "";
939
+ response.note =
940
+ existingNote +
941
+ `Requested ${String(sampleSize)} rows but only ${String(rows.length)} available.`;
942
+ }
943
+ return response;
903
944
  }
904
- // Add note if requested sampleSize exceeded available rows
905
- if (sampleSize !== undefined && rows.length < sampleSize) {
906
- const existingNote = response.note !== undefined ? response.note + " " : "";
907
- response.note =
908
- existingNote +
909
- `Requested ${String(sampleSize)} rows but only ${String(rows.length)} available.`;
945
+ catch (error) {
946
+ return {
947
+ success: false,
948
+ error: formatPostgresError(error, { tool: "pg_stats_sampling" }),
949
+ };
910
950
  }
911
- return response;
912
951
  },
913
952
  };
914
953
  }