@neverinfamous/postgres-mcp 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +202 -148
  2. package/dist/__tests__/benchmarks/codemode.bench.d.ts +10 -0
  3. package/dist/__tests__/benchmarks/codemode.bench.d.ts.map +1 -0
  4. package/dist/__tests__/benchmarks/codemode.bench.js +159 -0
  5. package/dist/__tests__/benchmarks/codemode.bench.js.map +1 -0
  6. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts +10 -0
  7. package/dist/__tests__/benchmarks/connection-pool.bench.d.ts.map +1 -0
  8. package/dist/__tests__/benchmarks/connection-pool.bench.js +123 -0
  9. package/dist/__tests__/benchmarks/connection-pool.bench.js.map +1 -0
  10. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts +11 -0
  11. package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts.map +1 -0
  12. package/dist/__tests__/benchmarks/handler-dispatch.bench.js +199 -0
  13. package/dist/__tests__/benchmarks/handler-dispatch.bench.js.map +1 -0
  14. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts +15 -0
  15. package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts.map +1 -0
  16. package/dist/__tests__/benchmarks/logger-sanitization.bench.js +155 -0
  17. package/dist/__tests__/benchmarks/logger-sanitization.bench.js.map +1 -0
  18. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts +10 -0
  19. package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts.map +1 -0
  20. package/dist/__tests__/benchmarks/resource-prompts.bench.js +181 -0
  21. package/dist/__tests__/benchmarks/resource-prompts.bench.js.map +1 -0
  22. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts +11 -0
  23. package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts.map +1 -0
  24. package/dist/__tests__/benchmarks/schema-parsing.bench.js +209 -0
  25. package/dist/__tests__/benchmarks/schema-parsing.bench.js.map +1 -0
  26. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts +9 -0
  27. package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts.map +1 -0
  28. package/dist/__tests__/benchmarks/tool-filtering.bench.js +83 -0
  29. package/dist/__tests__/benchmarks/tool-filtering.bench.js.map +1 -0
  30. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts +10 -0
  31. package/dist/__tests__/benchmarks/transport-auth.bench.d.ts.map +1 -0
  32. package/dist/__tests__/benchmarks/transport-auth.bench.js +128 -0
  33. package/dist/__tests__/benchmarks/transport-auth.bench.js.map +1 -0
  34. package/dist/__tests__/benchmarks/utilities.bench.d.ts +10 -0
  35. package/dist/__tests__/benchmarks/utilities.bench.d.ts.map +1 -0
  36. package/dist/__tests__/benchmarks/utilities.bench.js +164 -0
  37. package/dist/__tests__/benchmarks/utilities.bench.js.map +1 -0
  38. package/dist/adapters/DatabaseAdapter.d.ts.map +1 -1
  39. package/dist/adapters/DatabaseAdapter.js +12 -0
  40. package/dist/adapters/DatabaseAdapter.js.map +1 -1
  41. package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
  42. package/dist/adapters/postgresql/PostgresAdapter.js +56 -3
  43. package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
  44. package/dist/adapters/postgresql/prompts/ltree.js +2 -2
  45. package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
  46. package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
  47. package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
  48. package/dist/adapters/postgresql/schemas/admin.js +10 -5
  49. package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
  50. package/dist/adapters/postgresql/schemas/backup.d.ts +45 -27
  51. package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
  52. package/dist/adapters/postgresql/schemas/backup.js +64 -26
  53. package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
  54. package/dist/adapters/postgresql/schemas/core.d.ts +53 -19
  55. package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
  56. package/dist/adapters/postgresql/schemas/core.js +61 -17
  57. package/dist/adapters/postgresql/schemas/core.js.map +1 -1
  58. package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
  59. package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
  60. package/dist/adapters/postgresql/schemas/cron.js +64 -44
  61. package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
  62. package/dist/adapters/postgresql/schemas/extensions.d.ts +224 -110
  63. package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
  64. package/dist/adapters/postgresql/schemas/extensions.js +245 -96
  65. package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
  66. package/dist/adapters/postgresql/schemas/index.d.ts +7 -6
  67. package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
  68. package/dist/adapters/postgresql/schemas/index.js +16 -8
  69. package/dist/adapters/postgresql/schemas/index.js.map +1 -1
  70. package/dist/adapters/postgresql/schemas/introspection.d.ts +445 -0
  71. package/dist/adapters/postgresql/schemas/introspection.d.ts.map +1 -0
  72. package/dist/adapters/postgresql/schemas/introspection.js +478 -0
  73. package/dist/adapters/postgresql/schemas/introspection.js.map +1 -0
  74. package/dist/adapters/postgresql/schemas/jsonb.d.ts +102 -42
  75. package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
  76. package/dist/adapters/postgresql/schemas/jsonb.js +125 -30
  77. package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
  78. package/dist/adapters/postgresql/schemas/monitoring.d.ts +69 -36
  79. package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
  80. package/dist/adapters/postgresql/schemas/monitoring.js +98 -40
  81. package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
  82. package/dist/adapters/postgresql/schemas/partitioning.d.ts +21 -24
  83. package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
  84. package/dist/adapters/postgresql/schemas/partitioning.js +26 -14
  85. package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
  86. package/dist/adapters/postgresql/schemas/partman.d.ts +69 -0
  87. package/dist/adapters/postgresql/schemas/partman.d.ts.map +1 -1
  88. package/dist/adapters/postgresql/schemas/partman.js +46 -33
  89. package/dist/adapters/postgresql/schemas/partman.js.map +1 -1
  90. package/dist/adapters/postgresql/schemas/performance.d.ts +97 -49
  91. package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
  92. package/dist/adapters/postgresql/schemas/performance.js +139 -34
  93. package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
  94. package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
  95. package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
  96. package/dist/adapters/postgresql/schemas/postgis.js +40 -0
  97. package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
  98. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +50 -30
  99. package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
  100. package/dist/adapters/postgresql/schemas/schema-mgmt.js +105 -33
  101. package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
  102. package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
  103. package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
  104. package/dist/adapters/postgresql/schemas/stats.js +36 -20
  105. package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
  106. package/dist/adapters/postgresql/schemas/text-search.d.ts +34 -19
  107. package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
  108. package/dist/adapters/postgresql/schemas/text-search.js +52 -13
  109. package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
  110. package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
  111. package/dist/adapters/postgresql/tools/admin.js +272 -186
  112. package/dist/adapters/postgresql/tools/admin.js.map +1 -1
  113. package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
  114. package/dist/adapters/postgresql/tools/backup/dump.js +376 -350
  115. package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
  116. package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
  117. package/dist/adapters/postgresql/tools/citext.js +333 -243
  118. package/dist/adapters/postgresql/tools/citext.js.map +1 -1
  119. package/dist/adapters/postgresql/tools/codemode/index.d.ts.map +1 -1
  120. package/dist/adapters/postgresql/tools/codemode/index.js +2 -11
  121. package/dist/adapters/postgresql/tools/codemode/index.js.map +1 -1
  122. package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
  123. package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
  124. package/dist/adapters/postgresql/tools/core/convenience.js +101 -19
  125. package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
  126. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
  127. package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
  128. package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
  129. package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
  130. package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
  131. package/dist/adapters/postgresql/tools/core/health.js +18 -4
  132. package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
  133. package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
  134. package/dist/adapters/postgresql/tools/core/indexes.js +48 -6
  135. package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
  136. package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
  137. package/dist/adapters/postgresql/tools/core/objects.js +104 -85
  138. package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
  139. package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
  140. package/dist/adapters/postgresql/tools/core/query.js +100 -42
  141. package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
  142. package/dist/adapters/postgresql/tools/core/schemas.d.ts +51 -25
  143. package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
  144. package/dist/adapters/postgresql/tools/core/schemas.js +51 -25
  145. package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
  146. package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
  147. package/dist/adapters/postgresql/tools/core/tables.js +72 -32
  148. package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
  149. package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
  150. package/dist/adapters/postgresql/tools/cron.js +333 -206
  151. package/dist/adapters/postgresql/tools/cron.js.map +1 -1
  152. package/dist/adapters/postgresql/tools/introspection.d.ts +15 -0
  153. package/dist/adapters/postgresql/tools/introspection.d.ts.map +1 -0
  154. package/dist/adapters/postgresql/tools/introspection.js +1682 -0
  155. package/dist/adapters/postgresql/tools/introspection.js.map +1 -0
  156. package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
  157. package/dist/adapters/postgresql/tools/jsonb/advanced.js +394 -297
  158. package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
  159. package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
  160. package/dist/adapters/postgresql/tools/jsonb/basic.js +686 -398
  161. package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
  162. package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
  163. package/dist/adapters/postgresql/tools/kcache.js +278 -246
  164. package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
  165. package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
  166. package/dist/adapters/postgresql/tools/ltree.js +137 -38
  167. package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
  168. package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
  169. package/dist/adapters/postgresql/tools/monitoring.js +86 -55
  170. package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
  171. package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
  172. package/dist/adapters/postgresql/tools/partitioning.js +79 -15
  173. package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
  174. package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
  175. package/dist/adapters/postgresql/tools/partman/management.js +43 -56
  176. package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
  177. package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
  178. package/dist/adapters/postgresql/tools/partman/operations.js +137 -24
  179. package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
  180. package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
  181. package/dist/adapters/postgresql/tools/performance/analysis.js +276 -165
  182. package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
  183. package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
  184. package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
  185. package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
  186. package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
  187. package/dist/adapters/postgresql/tools/performance/monitoring.js +52 -12
  188. package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
  189. package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
  190. package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
  191. package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
  192. package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
  193. package/dist/adapters/postgresql/tools/performance/stats.js +182 -60
  194. package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
  195. package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
  196. package/dist/adapters/postgresql/tools/pgcrypto.js +277 -102
  197. package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
  198. package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
  199. package/dist/adapters/postgresql/tools/postgis/advanced.js +298 -230
  200. package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
  201. package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
  202. package/dist/adapters/postgresql/tools/postgis/basic.js +370 -251
  203. package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
  204. package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
  205. package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
  206. package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
  207. package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
  208. package/dist/adapters/postgresql/tools/schema.js +580 -233
  209. package/dist/adapters/postgresql/tools/schema.js.map +1 -1
  210. package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
  211. package/dist/adapters/postgresql/tools/stats/advanced.js +567 -506
  212. package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
  213. package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
  214. package/dist/adapters/postgresql/tools/stats/basic.js +340 -316
  215. package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
  216. package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
  217. package/dist/adapters/postgresql/tools/text.js +690 -337
  218. package/dist/adapters/postgresql/tools/text.js.map +1 -1
  219. package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
  220. package/dist/adapters/postgresql/tools/transactions.js +157 -50
  221. package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
  222. package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
  223. package/dist/adapters/postgresql/tools/vector/advanced.js +18 -0
  224. package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
  225. package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
  226. package/dist/adapters/postgresql/tools/vector/basic.js +100 -53
  227. package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
  228. package/dist/auth/auth-context.d.ts +28 -0
  229. package/dist/auth/auth-context.d.ts.map +1 -0
  230. package/dist/auth/auth-context.js +37 -0
  231. package/dist/auth/auth-context.js.map +1 -0
  232. package/dist/auth/scope-map.d.ts +20 -0
  233. package/dist/auth/scope-map.d.ts.map +1 -0
  234. package/dist/auth/scope-map.js +40 -0
  235. package/dist/auth/scope-map.js.map +1 -0
  236. package/dist/auth/scopes.d.ts.map +1 -1
  237. package/dist/auth/scopes.js +2 -0
  238. package/dist/auth/scopes.js.map +1 -1
  239. package/dist/cli.js +1 -1
  240. package/dist/cli.js.map +1 -1
  241. package/dist/codemode/api.d.ts +1 -0
  242. package/dist/codemode/api.d.ts.map +1 -1
  243. package/dist/codemode/api.js +35 -1
  244. package/dist/codemode/api.js.map +1 -1
  245. package/dist/codemode/index.d.ts +0 -2
  246. package/dist/codemode/index.d.ts.map +1 -1
  247. package/dist/codemode/index.js +0 -4
  248. package/dist/codemode/index.js.map +1 -1
  249. package/dist/codemode/sandbox.d.ts +14 -1
  250. package/dist/codemode/sandbox.d.ts.map +1 -1
  251. package/dist/codemode/sandbox.js +58 -19
  252. package/dist/codemode/sandbox.js.map +1 -1
  253. package/dist/codemode/types.d.ts.map +1 -1
  254. package/dist/codemode/types.js +3 -0
  255. package/dist/codemode/types.js.map +1 -1
  256. package/dist/constants/ServerInstructions.d.ts +5 -1
  257. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  258. package/dist/constants/ServerInstructions.js +117 -31
  259. package/dist/constants/ServerInstructions.js.map +1 -1
  260. package/dist/filtering/ToolConstants.d.ts +22 -19
  261. package/dist/filtering/ToolConstants.d.ts.map +1 -1
  262. package/dist/filtering/ToolConstants.js +48 -37
  263. package/dist/filtering/ToolConstants.js.map +1 -1
  264. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  265. package/dist/filtering/ToolFilter.js +10 -13
  266. package/dist/filtering/ToolFilter.js.map +1 -1
  267. package/dist/pool/ConnectionPool.js +1 -1
  268. package/dist/pool/ConnectionPool.js.map +1 -1
  269. package/dist/transports/http.d.ts +1 -0
  270. package/dist/transports/http.d.ts.map +1 -1
  271. package/dist/transports/http.js +75 -21
  272. package/dist/transports/http.js.map +1 -1
  273. package/dist/types/filtering.d.ts +2 -2
  274. package/dist/types/filtering.d.ts.map +1 -1
  275. package/dist/utils/icons.d.ts.map +1 -1
  276. package/dist/utils/icons.js +5 -0
  277. package/dist/utils/icons.js.map +1 -1
  278. package/dist/utils/where-clause.d.ts.map +1 -1
  279. package/dist/utils/where-clause.js +24 -0
  280. package/dist/utils/where-clause.js.map +1 -1
  281. package/package.json +20 -13
  282. package/dist/codemode/sandbox-factory.d.ts +0 -72
  283. package/dist/codemode/sandbox-factory.d.ts.map +0 -1
  284. package/dist/codemode/sandbox-factory.js +0 -88
  285. package/dist/codemode/sandbox-factory.js.map +0 -1
  286. package/dist/codemode/worker-sandbox.d.ts +0 -82
  287. package/dist/codemode/worker-sandbox.d.ts.map +0 -1
  288. package/dist/codemode/worker-sandbox.js +0 -244
  289. package/dist/codemode/worker-sandbox.js.map +0 -1
  290. package/dist/codemode/worker-script.d.ts +0 -8
  291. package/dist/codemode/worker-script.d.ts.map +0 -1
  292. package/dist/codemode/worker-script.js +0 -113
  293. package/dist/codemode/worker-script.js.map +0 -1
@@ -5,6 +5,8 @@
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";
9
+ import { sanitizeWhereClause } from "../../../../utils/where-clause.js";
8
10
  import {
9
11
  // Base schemas for MCP visibility
10
12
  StatsTimeSeriesSchemaBase, StatsDistributionSchemaBase, StatsHypothesisSchemaBase, StatsSamplingSchemaBase,
@@ -182,21 +184,28 @@ async function validateNumericColumn(adapter, table, column, schema) {
182
184
  "money",
183
185
  ];
184
186
  const typeCheckQuery = `
185
- SELECT data_type
186
- FROM information_schema.columns
187
- WHERE table_schema = '${schema}'
188
- AND table_name = '${table}'
189
- AND column_name = '${column}'
187
+ SELECT data_type
188
+ FROM information_schema.columns
189
+ WHERE table_schema = $1
190
+ AND table_name = $2
191
+ AND column_name = $3
190
192
  `;
191
- const typeResult = await adapter.executeQuery(typeCheckQuery);
193
+ const typeResult = await adapter.executeQuery(typeCheckQuery, [
194
+ schema,
195
+ table,
196
+ column,
197
+ ]);
192
198
  const typeRow = typeResult.rows?.[0];
193
199
  if (!typeRow) {
194
200
  // Check if table exists
195
201
  const tableCheckQuery = `
196
- SELECT 1 FROM information_schema.tables
197
- WHERE table_schema = '${schema}' AND table_name = '${table}'
202
+ SELECT 1 FROM information_schema.tables
203
+ WHERE table_schema = $1 AND table_name = $2
198
204
  `;
199
- const tableResult = await adapter.executeQuery(tableCheckQuery);
205
+ const tableResult = await adapter.executeQuery(tableCheckQuery, [
206
+ schema,
207
+ table,
208
+ ]);
200
209
  if (tableResult.rows?.length === 0) {
201
210
  throw new Error(`Table "${schema}.${table}" not found`);
202
211
  }
@@ -212,10 +221,13 @@ async function validateNumericColumn(adapter, table, column, schema) {
212
221
  */
213
222
  async function validateTableExists(adapter, table, schema) {
214
223
  const tableCheckQuery = `
215
- SELECT 1 FROM information_schema.tables
216
- WHERE table_schema = '${schema}' AND table_name = '${table}'
224
+ SELECT 1 FROM information_schema.tables
225
+ WHERE table_schema = $1 AND table_name = $2
217
226
  `;
218
- const tableResult = await adapter.executeQuery(tableCheckQuery);
227
+ const tableResult = await adapter.executeQuery(tableCheckQuery, [
228
+ schema,
229
+ table,
230
+ ]);
219
231
  if (tableResult.rows?.length === 0) {
220
232
  throw new Error(`Table "${schema}.${table}" not found`);
221
233
  }
@@ -236,116 +248,130 @@ export function createStatsTimeSeriesTool(adapter) {
236
248
  annotations: readOnly("Time Series Analysis"),
237
249
  icons: getToolIcons("stats", readOnly("Time Series Analysis")),
238
250
  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 = `
253
- SELECT 1 FROM information_schema.tables
254
- WHERE table_schema = '${schemaName}' AND table_name = '${table}'
251
+ try {
252
+ const { table, valueColumn, timeColumn, interval, aggregation, schema, where, params: queryParams, limit, groupBy, groupLimit, } = StatsTimeSeriesSchema.parse(params);
253
+ const schemaPrefix = schema ? `"${schema}".` : "";
254
+ const whereClause = where ? `WHERE ${sanitizeWhereClause(where)}` : "";
255
+ const agg = aggregation ?? "avg";
256
+ // Handle limit: undefined uses default (100), 0 means no limit
257
+ // Track whether user explicitly provided a limit
258
+ const userProvidedLimit = limit !== undefined;
259
+ const DEFAULT_LIMIT = 100;
260
+ // limit === 0 means "no limit", otherwise use provided limit or default
261
+ const effectiveLimit = limit === 0 ? undefined : (limit ?? DEFAULT_LIMIT);
262
+ const usingDefaultLimit = !userProvidedLimit && effectiveLimit !== undefined;
263
+ // First check if table exists
264
+ const schemaName = schema ?? "public";
265
+ const tableCheckQuery = `
266
+ SELECT 1 FROM information_schema.tables
267
+ WHERE table_schema = $1 AND table_name = $2
255
268
  `;
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 = `
262
- SELECT data_type
263
- FROM information_schema.columns
264
- WHERE table_schema = '${schemaName}'
265
- AND table_name = '${table}'
266
- AND column_name = '${timeColumn}'
269
+ const tableCheckResult = await adapter.executeQuery(tableCheckQuery, [
270
+ schemaName,
271
+ table,
272
+ ]);
273
+ if (tableCheckResult.rows?.length === 0) {
274
+ throw new Error(`Table "${schemaName}.${table}" not found`);
275
+ }
276
+ // Validate timeColumn is a timestamp/date type
277
+ const typeCheckQuery = `
278
+ SELECT data_type
279
+ FROM information_schema.columns
280
+ WHERE table_schema = $1
281
+ AND table_name = $2
282
+ AND column_name = $3
267
283
  `;
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 = `
297
- SELECT data_type
298
- FROM information_schema.columns
299
- WHERE table_schema = '${schemaName}'
300
- AND table_name = '${table}'
301
- AND column_name = '${valueColumn}'
302
- `;
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();
284
+ const typeResult = await adapter.executeQuery(typeCheckQuery, [
285
+ schemaName,
286
+ table,
287
+ timeColumn,
288
+ ]);
289
+ const typeRow = typeResult.rows?.[0];
290
+ if (!typeRow) {
291
+ throw new Error(`Column "${timeColumn}" not found in table "${schemaName}.${table}"`);
318
292
  }
319
- else if (typeof timeBucketValue === "string") {
320
- timeBucket = timeBucketValue;
293
+ const validTypes = [
294
+ "timestamp without time zone",
295
+ "timestamp with time zone",
296
+ "date",
297
+ "time",
298
+ "time without time zone",
299
+ "time with time zone",
300
+ ];
301
+ if (!validTypes.includes(typeRow.data_type)) {
302
+ throw new Error(`Column "${timeColumn}" is type "${typeRow.data_type}" but must be a timestamp or date type for time series analysis`);
321
303
  }
322
- else {
323
- // Fallback: null, undefined, or unexpected type
324
- timeBucket = "";
304
+ // Note: schemaName already defined above for table check
305
+ // Validate valueColumn exists and is numeric
306
+ const numericTypes = [
307
+ "integer",
308
+ "bigint",
309
+ "smallint",
310
+ "numeric",
311
+ "decimal",
312
+ "real",
313
+ "double precision",
314
+ "money",
315
+ ];
316
+ const valueTypeQuery = `
317
+ SELECT data_type
318
+ FROM information_schema.columns
319
+ WHERE table_schema = $1
320
+ AND table_name = $2
321
+ AND column_name = $3
322
+ `;
323
+ const valueTypeResult = await adapter.executeQuery(valueTypeQuery, [
324
+ schemaName,
325
+ table,
326
+ valueColumn,
327
+ ]);
328
+ const valueTypeRow = valueTypeResult.rows?.[0];
329
+ if (!valueTypeRow) {
330
+ throw new Error(`Column "${valueColumn}" not found in table "${schemaName}.${table}"`);
325
331
  }
326
- return {
327
- timeBucket,
328
- value: Number(row["value"]),
329
- count: Number(row["count"]),
332
+ if (!numericTypes.includes(valueTypeRow.data_type)) {
333
+ throw new Error(`Column "${valueColumn}" is type "${valueTypeRow.data_type}" but must be a numeric type for time series aggregation`);
334
+ }
335
+ // Helper to map bucket row - convert Date to ISO string for JSON Schema
336
+ // Handles both Date objects (from real DB) and strings (from mocks)
337
+ const mapBucket = (row) => {
338
+ const timeBucketValue = row["time_bucket"];
339
+ let timeBucket;
340
+ if (timeBucketValue instanceof Date) {
341
+ timeBucket = timeBucketValue.toISOString();
342
+ }
343
+ else if (typeof timeBucketValue === "string") {
344
+ timeBucket = timeBucketValue;
345
+ }
346
+ else {
347
+ // Fallback: null, undefined, or unexpected type
348
+ timeBucket = "";
349
+ }
350
+ return {
351
+ timeBucket,
352
+ value: Number(row["value"]),
353
+ count: Number(row["count"]),
354
+ };
330
355
  };
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
356
+ if (groupBy !== undefined) {
357
+ // Handle groupLimit: undefined uses default (20), 0 means no limit
358
+ const DEFAULT_GROUP_LIMIT = 20;
359
+ const userProvidedGroupLimit = groupLimit !== undefined;
360
+ const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
361
+ // First get total count of distinct groups for truncation indicator
362
+ // COUNT(DISTINCT) excludes NULLs per SQL standard, so add 1 if any NULLs exist
363
+ const groupCountSql = `
364
+ SELECT COUNT(DISTINCT "${groupBy}") +
365
+ CASE WHEN COUNT(*) > COUNT("${groupBy}") THEN 1 ELSE 0 END as total_groups
340
366
  FROM ${schemaPrefix}"${table}"
341
367
  ${whereClause}
342
368
  `;
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 = `
348
- SELECT
369
+ const groupCountResult = await adapter.executeQuery(groupCountSql);
370
+ const totalGroupCount = Number(groupCountResult.rows?.[0]
371
+ ?.total_groups ?? 0);
372
+ // Grouped time series
373
+ const sql = `
374
+ SELECT
349
375
  "${groupBy}" as group_key,
350
376
  DATE_TRUNC('${interval}', "${timeColumn}") as time_bucket,
351
377
  ${agg.toUpperCase()}("${valueColumn}")::numeric(20,6) as value,
@@ -355,78 +381,79 @@ export function createStatsTimeSeriesTool(adapter) {
355
381
  GROUP BY "${groupBy}", DATE_TRUNC('${interval}', "${timeColumn}")
356
382
  ORDER BY "${groupBy}", time_bucket DESC
357
383
  `;
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;
384
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
385
+ ? [queryParams]
386
+ : []));
387
+ const rows = result.rows ?? [];
388
+ // Group results by group_key
389
+ const groupsMap = new Map();
390
+ const groupsTotalCount = new Map();
391
+ let groupsProcessed = 0;
392
+ for (const row of rows) {
393
+ const key = row["group_key"];
394
+ if (!groupsMap.has(key)) {
395
+ // Check if we've hit the group limit
396
+ if (effectiveGroupLimit !== undefined &&
397
+ groupsProcessed >= effectiveGroupLimit) {
398
+ continue;
399
+ }
400
+ groupsMap.set(key, []);
401
+ groupsTotalCount.set(key, 0);
402
+ groupsProcessed++;
403
+ }
404
+ const currentTotal = groupsTotalCount.get(key) ?? 0;
405
+ groupsTotalCount.set(key, currentTotal + 1);
406
+ const bucketList = groupsMap.get(key);
407
+ // Only add if no limit or under limit
408
+ if (bucketList !== undefined &&
409
+ (effectiveLimit === undefined ||
410
+ bucketList.length < effectiveLimit)) {
411
+ bucketList.push(mapBucket(row));
373
412
  }
374
- groupsMap.set(key, []);
375
- groupsTotalCount.set(key, 0);
376
- groupsProcessed++;
377
413
  }
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));
414
+ const groups = Array.from(groupsMap.entries()).map(([key, buckets]) => ({
415
+ groupKey: key,
416
+ buckets,
417
+ }));
418
+ // Build response with truncation indicators
419
+ const response = {
420
+ table: `${schema ?? "public"}.${table}`,
421
+ valueColumn,
422
+ timeColumn,
423
+ interval,
424
+ aggregation: agg,
425
+ groupBy,
426
+ groups,
427
+ count: groups.length,
428
+ };
429
+ // Add truncation indicators when groups are limited
430
+ const groupsTruncated = effectiveGroupLimit !== undefined &&
431
+ totalGroupCount > effectiveGroupLimit;
432
+ if (groupsTruncated || !userProvidedGroupLimit) {
433
+ response["truncated"] = groupsTruncated;
434
+ response["totalGroupCount"] = totalGroupCount;
385
435
  }
436
+ return response;
386
437
  }
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 = `
438
+ // Ungrouped time series
439
+ // Build LIMIT clause: no LIMIT if effectiveLimit is undefined (limit: 0)
440
+ const limitClause = effectiveLimit !== undefined ? `LIMIT ${String(effectiveLimit)}` : "";
441
+ // Get total count if using default limit (for truncation indicator)
442
+ let totalCount;
443
+ if (usingDefaultLimit) {
444
+ const countSql = `
418
445
  SELECT COUNT(DISTINCT DATE_TRUNC('${interval}', "${timeColumn}")) as total_buckets
419
446
  FROM ${schemaPrefix}"${table}"
420
447
  ${whereClause}
421
448
  `;
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 = `
429
- SELECT
449
+ const countResult = await adapter.executeQuery(countSql, ...(queryParams !== undefined && queryParams.length > 0
450
+ ? [queryParams]
451
+ : []));
452
+ const countRow = countResult.rows?.[0];
453
+ totalCount = countRow ? Number(countRow.total_buckets) : undefined;
454
+ }
455
+ const sql = `
456
+ SELECT
430
457
  DATE_TRUNC('${interval}', "${timeColumn}") as time_bucket,
431
458
  ${agg.toUpperCase()}("${valueColumn}")::numeric(20,6) as value,
432
459
  COUNT(*) as count
@@ -436,25 +463,32 @@ export function createStatsTimeSeriesTool(adapter) {
436
463
  ORDER BY time_bucket DESC
437
464
  ${limitClause}
438
465
  `;
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;
466
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
467
+ ? [queryParams]
468
+ : []));
469
+ const buckets = (result.rows ?? []).map((row) => mapBucket(row));
470
+ // Build response
471
+ const response = {
472
+ table: `${schema ?? "public"}.${table}`,
473
+ valueColumn,
474
+ timeColumn,
475
+ interval,
476
+ aggregation: agg,
477
+ buckets,
478
+ };
479
+ // Add truncation indicators when default limit was applied
480
+ if (usingDefaultLimit && totalCount !== undefined) {
481
+ response["truncated"] = buckets.length < totalCount;
482
+ response["totalCount"] = totalCount;
483
+ }
484
+ return response;
485
+ }
486
+ catch (error) {
487
+ return {
488
+ success: false,
489
+ error: formatPostgresError(error, { tool: "pg_stats_time_series" }),
490
+ };
456
491
  }
457
- return response;
458
492
  },
459
493
  };
460
494
  }
@@ -471,24 +505,25 @@ export function createStatsDistributionTool(adapter) {
471
505
  annotations: readOnly("Distribution Analysis"),
472
506
  icons: getToolIcons("stats", readOnly("Distribution Analysis")),
473
507
  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 = `
508
+ try {
509
+ const parsed = StatsDistributionSchema.parse(params);
510
+ const { table, column, buckets, schema, where, params: queryParams, groupBy, groupLimit, } = parsed;
511
+ const schemaName = schema ?? "public";
512
+ const schemaPrefix = schema ? `"${schema}".` : "";
513
+ const whereClause = where ? `WHERE ${sanitizeWhereClause(where)}` : "";
514
+ const numBuckets = buckets ?? 10;
515
+ // Validate column exists and is numeric
516
+ await validateNumericColumn(adapter, table, column, schemaName);
517
+ // Helper to compute skewness and kurtosis for a given group
518
+ const computeMoments = async (groupFilter) => {
519
+ const filterClause = groupFilter
520
+ ? whereClause
521
+ ? `${whereClause} AND ${groupFilter}`
522
+ : `WHERE ${groupFilter}`
523
+ : whereClause;
524
+ const statsQuery = `
490
525
  WITH stats AS (
491
- SELECT
526
+ SELECT
492
527
  MIN("${column}") as min_val,
493
528
  MAX("${column}") as max_val,
494
529
  AVG("${column}") as mean,
@@ -498,7 +533,7 @@ export function createStatsDistributionTool(adapter) {
498
533
  ${filterClause}
499
534
  ),
500
535
  moments AS (
501
- SELECT
536
+ SELECT
502
537
  s.min_val,
503
538
  s.max_val,
504
539
  s.mean,
@@ -516,29 +551,29 @@ export function createStatsDistributionTool(adapter) {
516
551
  )
517
552
  SELECT * FROM moments
518
553
  `;
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,
554
+ const result = await adapter.executeQuery(statsQuery, ...(queryParams !== undefined && queryParams.length > 0
555
+ ? [queryParams]
556
+ : []));
557
+ const row = result.rows?.[0];
558
+ if (row?.["min_val"] == null || row["max_val"] == null) {
559
+ return null;
560
+ }
561
+ return {
562
+ minVal: Number(row["min_val"]),
563
+ maxVal: Number(row["max_val"]),
564
+ skewness: row["skewness"] !== null ? Number(row["skewness"]) : null,
565
+ kurtosis: row["kurtosis"] !== null ? Number(row["kurtosis"]) : null,
566
+ };
531
567
  };
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 = `
541
- SELECT
568
+ // Helper to generate histogram for given min/max
569
+ const generateHistogram = async (minVal, maxVal, groupFilter) => {
570
+ const filterClause = groupFilter
571
+ ? whereClause
572
+ ? `${whereClause} AND ${groupFilter}`
573
+ : `WHERE ${groupFilter}`
574
+ : whereClause;
575
+ const histogramQuery = `
576
+ SELECT
542
577
  WIDTH_BUCKET("${column}", ${String(minVal)}, ${String(maxVal + 0.0001)}, ${String(numBuckets)}) as bucket,
543
578
  COUNT(*) as frequency,
544
579
  MIN("${column}") as bucket_min,
@@ -548,92 +583,99 @@ export function createStatsDistributionTool(adapter) {
548
583
  GROUP BY WIDTH_BUCKET("${column}", ${String(minVal)}, ${String(maxVal + 0.0001)}, ${String(numBuckets)})
549
584
  ORDER BY bucket
550
585
  `;
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 = `
586
+ const result = await adapter.executeQuery(histogramQuery, ...(queryParams !== undefined && queryParams.length > 0
587
+ ? [queryParams]
588
+ : []));
589
+ return (result.rows ?? []).map((row) => ({
590
+ bucket: Number(row["bucket"]),
591
+ frequency: Number(row["frequency"]),
592
+ rangeMin: Number(row["bucket_min"]),
593
+ rangeMax: Number(row["bucket_max"]),
594
+ }));
595
+ };
596
+ if (groupBy !== undefined) {
597
+ // Handle groupLimit: undefined uses default (20), 0 means no limit
598
+ const DEFAULT_GROUP_LIMIT = 20;
599
+ const userProvidedGroupLimit = groupLimit !== undefined;
600
+ const effectiveGroupLimit = groupLimit === 0 ? undefined : (groupLimit ?? DEFAULT_GROUP_LIMIT);
601
+ // Get distinct groups first
602
+ const groupsQuery = `
568
603
  SELECT DISTINCT "${groupBy}" as group_key
569
604
  FROM ${schemaPrefix}"${table}"
570
605
  ${whereClause}
571
606
  ORDER BY "${groupBy}"
572
607
  `;
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
- });
608
+ const groupsResult = await adapter.executeQuery(groupsQuery, ...(queryParams !== undefined && queryParams.length > 0
609
+ ? [queryParams]
610
+ : []));
611
+ const allGroupKeys = (groupsResult.rows ?? []).map((r) => r["group_key"]);
612
+ const totalGroupCount = allGroupKeys.length;
613
+ // Apply group limit
614
+ const groupKeys = effectiveGroupLimit !== undefined
615
+ ? allGroupKeys.slice(0, effectiveGroupLimit)
616
+ : allGroupKeys;
617
+ // Process each group
618
+ const groups = [];
619
+ for (const groupKey of groupKeys) {
620
+ const groupFilter = typeof groupKey === "string"
621
+ ? `"${groupBy}" = '${groupKey.replace(/'/g, "''")}'`
622
+ : `"${groupBy}" = ${String(groupKey)}`;
623
+ const moments = await computeMoments(groupFilter);
624
+ if (moments === null)
625
+ continue;
626
+ const { minVal, maxVal, skewness, kurtosis } = moments;
627
+ const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
628
+ const histogram = await generateHistogram(minVal, maxVal, groupFilter);
629
+ groups.push({
630
+ groupKey,
631
+ range: { min: minVal, max: maxVal },
632
+ bucketWidth,
633
+ skewness,
634
+ kurtosis,
635
+ histogram,
636
+ });
637
+ }
638
+ // Build response with truncation indicators
639
+ const response = {
640
+ table: `${schema ?? "public"}.${table}`,
641
+ column,
642
+ groupBy,
643
+ groups,
644
+ count: groups.length,
645
+ };
646
+ // Add truncation indicators when groups are limited
647
+ const groupsTruncated = effectiveGroupLimit !== undefined &&
648
+ totalGroupCount > effectiveGroupLimit;
649
+ if (groupsTruncated || !userProvidedGroupLimit) {
650
+ response["truncated"] = groupsTruncated;
651
+ response["totalGroupCount"] = totalGroupCount;
652
+ }
653
+ return response;
602
654
  }
603
- // Build response with truncation indicators
604
- const response = {
655
+ // Ungrouped distribution (existing logic)
656
+ const moments = await computeMoments();
657
+ if (moments === null) {
658
+ return { error: "No data or all nulls in column" };
659
+ }
660
+ const { minVal, maxVal, skewness, kurtosis } = moments;
661
+ const bucketWidth = Math.round(((maxVal - minVal) / numBuckets) * 1e6) / 1e6;
662
+ const histogram = await generateHistogram(minVal, maxVal);
663
+ return {
605
664
  table: `${schema ?? "public"}.${table}`,
606
665
  column,
607
- groupBy,
608
- groups,
609
- count: groups.length,
666
+ range: { min: minVal, max: maxVal },
667
+ bucketWidth,
668
+ skewness,
669
+ kurtosis,
670
+ histogram,
610
671
  };
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
672
  }
620
- // Ungrouped distribution (existing logic)
621
- const moments = await computeMoments();
622
- if (moments === null) {
623
- return { error: "No data or all nulls in column" };
673
+ catch (error) {
674
+ return {
675
+ success: false,
676
+ error: formatPostgresError(error, { tool: "pg_stats_distribution" }),
677
+ };
624
678
  }
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
679
  },
638
680
  };
639
681
  }
@@ -650,88 +692,92 @@ export function createStatsHypothesisTool(adapter) {
650
692
  annotations: readOnly("Hypothesis Testing"),
651
693
  icons: getToolIcons("stats", readOnly("Hypothesis Testing")),
652
694
  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;
695
+ try {
696
+ const { table, column, testType, hypothesizedMean, populationStdDev, schema, where, params: queryParams, groupBy, } = StatsHypothesisSchema.parse(params);
697
+ const schemaName = schema ?? "public";
698
+ const schemaPrefix = schema ? `"${schema}".` : "";
699
+ const whereClause = where ? `WHERE ${sanitizeWhereClause(where)}` : "";
700
+ // Validate column exists and is numeric
701
+ await validateNumericColumn(adapter, table, column, schemaName);
702
+ // Helper to calculate test results from row stats
703
+ const calculateTestResults = (n, sampleMean, sampleStdDev) => {
704
+ if (n < 2 || isNaN(sampleStdDev) || sampleStdDev === 0) {
705
+ return {
706
+ error: "Insufficient data or zero variance",
707
+ sampleSize: n,
708
+ };
709
+ }
710
+ let stddevUsed;
711
+ let stddevNote;
712
+ if (testType === "z_test") {
713
+ if (populationStdDev !== undefined) {
714
+ stddevUsed = populationStdDev;
715
+ }
716
+ else {
717
+ stddevUsed = sampleStdDev;
718
+ stddevNote =
719
+ "No populationStdDev provided; using sample stddev (less accurate for z-test)";
720
+ }
669
721
  }
670
722
  else {
671
723
  stddevUsed = sampleStdDev;
672
- stddevNote =
673
- "No populationStdDev provided; using sample stddev (less accurate for z-test)";
674
724
  }
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,
725
+ const standardError = stddevUsed / Math.sqrt(n);
726
+ const testStatistic = (sampleMean - hypothesizedMean) / standardError;
727
+ const degreesOfFreedom = n - 1;
728
+ // Calculate p-value based on test type
729
+ const pValue = testType === "z_test"
730
+ ? calculateZTestPValue(testStatistic)
731
+ : calculateTTestPValue(testStatistic, degreesOfFreedom);
732
+ // Round p-value to 6 decimal places for cleaner output
733
+ const pValueRounded = Math.round(pValue * 1e6) / 1e6;
734
+ // Determine significance based on p-value
735
+ let interpretation;
736
+ if (pValueRounded < 0.001) {
737
+ interpretation =
738
+ "Highly significant (p < 0.001): Strong evidence against the null hypothesis";
739
+ }
740
+ else if (pValueRounded < 0.01) {
741
+ interpretation =
742
+ "Very significant (p < 0.01): Strong evidence against the null hypothesis";
743
+ }
744
+ else if (pValueRounded < 0.05) {
745
+ interpretation =
746
+ "Significant (p < 0.05): Evidence against the null hypothesis at α=0.05 level";
747
+ }
748
+ else if (pValueRounded < 0.1) {
749
+ interpretation =
750
+ "Marginally significant (p < 0.1): Weak evidence against the null hypothesis";
751
+ }
752
+ else {
753
+ interpretation =
754
+ "Not significant (p 0.1): Insufficient evidence to reject the null hypothesis";
755
+ }
756
+ // Build note with warnings
757
+ let noteText = stddevNote ??
758
+ "Two-tailed p-value calculated using numerical approximation";
759
+ if (n < 30) {
760
+ noteText =
761
+ `Small sample size (n=${String(n)}): results may be less reliable. ` +
762
+ noteText;
763
+ }
764
+ return {
765
+ sampleSize: n,
766
+ sampleMean,
767
+ sampleStdDev,
768
+ populationStdDev: testType === "z_test" ? (populationStdDev ?? null) : null,
769
+ standardError,
770
+ testStatistic,
771
+ pValue: pValueRounded,
772
+ degreesOfFreedom: testType === "t_test" ? degreesOfFreedom : null,
773
+ interpretation,
774
+ note: noteText,
775
+ };
729
776
  };
730
- };
731
- if (groupBy !== undefined) {
732
- // Grouped hypothesis tests
733
- const sql = `
734
- SELECT
777
+ if (groupBy !== undefined) {
778
+ // Grouped hypothesis tests
779
+ const sql = `
780
+ SELECT
735
781
  "${groupBy}" as group_key,
736
782
  COUNT("${column}") as n,
737
783
  AVG("${column}")::numeric(20,6) as mean,
@@ -741,59 +787,66 @@ export function createStatsHypothesisTool(adapter) {
741
787
  GROUP BY "${groupBy}"
742
788
  ORDER BY "${groupBy}"
743
789
  `;
790
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
791
+ ? [queryParams]
792
+ : []));
793
+ const rows = result.rows ?? [];
794
+ const groups = rows.map((row) => {
795
+ const n = Number(row["n"]);
796
+ const sampleMean = Number(row["mean"]);
797
+ const sampleStdDev = Number(row["stddev"]);
798
+ return {
799
+ groupKey: row["group_key"],
800
+ results: calculateTestResults(n, sampleMean, sampleStdDev),
801
+ };
802
+ });
803
+ return {
804
+ table: `${schema ?? "public"}.${table}`,
805
+ column,
806
+ testType,
807
+ hypothesizedMean,
808
+ groupBy,
809
+ groups,
810
+ count: groups.length,
811
+ };
812
+ }
813
+ // Ungrouped hypothesis test
814
+ const sql = `
815
+ SELECT
816
+ COUNT("${column}") as n,
817
+ AVG("${column}")::numeric(20,6) as mean,
818
+ STDDEV_SAMP("${column}")::numeric(20,6) as stddev
819
+ FROM ${schemaPrefix}"${table}"
820
+ ${whereClause}
821
+ `;
744
822
  const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
745
823
  ? [queryParams]
746
824
  : []));
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
- });
825
+ const row = result.rows?.[0];
826
+ if (!row)
827
+ return { error: "No data found" };
828
+ const n = Number(row.n);
829
+ const sampleMean = Number(row.mean);
830
+ const sampleStdDev = Number(row.stddev);
831
+ const testResults = calculateTestResults(n, sampleMean, sampleStdDev);
832
+ // If error, return at top level (not nested in results)
833
+ if ("error" in testResults) {
834
+ return testResults;
835
+ }
757
836
  return {
758
837
  table: `${schema ?? "public"}.${table}`,
759
838
  column,
760
839
  testType,
761
840
  hypothesizedMean,
762
- groupBy,
763
- groups,
764
- count: groups.length,
841
+ results: testResults,
765
842
  };
766
843
  }
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;
844
+ catch (error) {
845
+ return {
846
+ success: false,
847
+ error: formatPostgresError(error, { tool: "pg_stats_hypothesis" }),
848
+ };
789
849
  }
790
- return {
791
- table: `${schema ?? "public"}.${table}`,
792
- column,
793
- testType,
794
- hypothesizedMean,
795
- results: testResults,
796
- };
797
850
  },
798
851
  };
799
852
  }
@@ -810,105 +863,113 @@ export function createStatsSamplingTool(adapter) {
810
863
  annotations: readOnly("Random Sampling"),
811
864
  icons: getToolIcons("stats", readOnly("Random Sampling")),
812
865
  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 = `
866
+ try {
867
+ const { table, method, sampleSize, percentage, schema, select, where, params: queryParams, } = StatsSamplingSchema.parse(params);
868
+ const schemaName = schema ?? "public";
869
+ // Validate table exists
870
+ await validateTableExists(adapter, table, schemaName);
871
+ const schemaPrefix = schema ? `"${schema}".` : "";
872
+ const columns = select && select.length > 0
873
+ ? select.map((c) => `"${c}"`).join(", ")
874
+ : "*";
875
+ const whereClause = where ? `WHERE ${sanitizeWhereClause(where)}` : "";
876
+ const samplingMethod = method ?? "random";
877
+ let sql;
878
+ let note;
879
+ // If sampleSize is provided, always use ORDER BY RANDOM() LIMIT n for exact counts
880
+ // TABLESAMPLE BERNOULLI/SYSTEM are percentage-based and cannot guarantee exact row counts
881
+ if (sampleSize !== undefined) {
882
+ const limit = sampleSize;
883
+ sql = `
830
884
  SELECT ${columns}
831
885
  FROM ${schemaPrefix}"${table}"
832
886
  ${whereClause}
833
887
  ORDER BY RANDOM()
834
888
  LIMIT ${String(limit)}
835
889
  `;
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.`;
890
+ if (percentage !== undefined) {
891
+ note = `sampleSize (${String(sampleSize)}) takes precedence over percentage (${String(percentage)}%). Using ORDER BY RANDOM() LIMIT for exact row count.`;
892
+ }
893
+ else if (samplingMethod !== "random") {
894
+ note = `Using ORDER BY RANDOM() LIMIT for exact ${String(sampleSize)} row count. TABLESAMPLE ${samplingMethod.toUpperCase()} is percentage-based and cannot guarantee exact counts.`;
895
+ }
841
896
  }
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 = `
897
+ else if (samplingMethod === "random") {
898
+ // Default random sampling with default limit (20 to reduce LLM context usage)
899
+ const limit = 20;
900
+ sql = `
847
901
  SELECT ${columns}
848
902
  FROM ${schemaPrefix}"${table}"
849
903
  ${whereClause}
850
904
  ORDER BY RANDOM()
851
905
  LIMIT ${String(limit)}
852
906
  `;
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.`;
907
+ if (percentage !== undefined) {
908
+ 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.`;
909
+ }
855
910
  }
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 = `
911
+ else {
912
+ // TABLESAMPLE with percentage (approximate row count)
913
+ // Apply default limit to prevent large payloads
914
+ const pct = percentage ?? 10;
915
+ const DEFAULT_TABLESAMPLE_LIMIT = 100;
916
+ sql = `
863
917
  SELECT ${columns}
864
918
  FROM ${schemaPrefix}"${table}"
865
919
  TABLESAMPLE ${samplingMethod.toUpperCase()}(${String(pct)})
866
920
  ${whereClause}
867
921
  LIMIT ${String(DEFAULT_TABLESAMPLE_LIMIT + 1)}
868
922
  `;
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;
923
+ // Add hint about system method unreliability for small tables
924
+ const methodHint = samplingMethod === "system"
925
+ ? " Consider using 'bernoulli' or 'random' method for more reliable results on small tables."
926
+ : "";
927
+ note = `TABLESAMPLE ${samplingMethod.toUpperCase()}(${String(pct)}%) returns approximately ${String(pct)}% of rows. Actual count varies based on table size and sampling algorithm.${methodHint}`;
928
+ }
929
+ const result = await adapter.executeQuery(sql, ...(queryParams !== undefined && queryParams.length > 0
930
+ ? [queryParams]
931
+ : []));
932
+ let rows = result.rows ?? [];
933
+ // Check if we need to truncate due to default limit for TABLESAMPLE methods
934
+ let truncated = false;
935
+ let totalSampled;
936
+ const DEFAULT_TABLESAMPLE_LIMIT = 100;
937
+ if (sampleSize === undefined &&
938
+ samplingMethod !== "random" &&
939
+ rows.length > DEFAULT_TABLESAMPLE_LIMIT) {
940
+ totalSampled = rows.length;
941
+ rows = rows.slice(0, DEFAULT_TABLESAMPLE_LIMIT);
942
+ truncated = true;
943
+ }
944
+ const response = {
945
+ table: `${schema ?? "public"}.${table}`,
946
+ method: samplingMethod,
947
+ sampleSize: rows.length,
948
+ rows,
949
+ };
950
+ // Add truncation indicators if applicable
951
+ if (truncated && totalSampled !== undefined) {
952
+ response.truncated = truncated;
953
+ response.totalSampled = totalSampled;
954
+ }
955
+ if (note !== undefined) {
956
+ response.note = note;
957
+ }
958
+ // Add note if requested sampleSize exceeded available rows
959
+ if (sampleSize !== undefined && rows.length < sampleSize) {
960
+ const existingNote = response.note !== undefined ? response.note + " " : "";
961
+ response.note =
962
+ existingNote +
963
+ `Requested ${String(sampleSize)} rows but only ${String(rows.length)} available.`;
964
+ }
965
+ return response;
903
966
  }
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.`;
967
+ catch (error) {
968
+ return {
969
+ success: false,
970
+ error: formatPostgresError(error, { tool: "pg_stats_sampling" }),
971
+ };
910
972
  }
911
- return response;
912
973
  },
913
974
  };
914
975
  }