@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.
- package/README.md +202 -148
- package/dist/__tests__/benchmarks/codemode.bench.d.ts +10 -0
- package/dist/__tests__/benchmarks/codemode.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/codemode.bench.js +159 -0
- package/dist/__tests__/benchmarks/codemode.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/connection-pool.bench.d.ts +10 -0
- package/dist/__tests__/benchmarks/connection-pool.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/connection-pool.bench.js +123 -0
- package/dist/__tests__/benchmarks/connection-pool.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts +11 -0
- package/dist/__tests__/benchmarks/handler-dispatch.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/handler-dispatch.bench.js +199 -0
- package/dist/__tests__/benchmarks/handler-dispatch.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts +15 -0
- package/dist/__tests__/benchmarks/logger-sanitization.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/logger-sanitization.bench.js +155 -0
- package/dist/__tests__/benchmarks/logger-sanitization.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts +10 -0
- package/dist/__tests__/benchmarks/resource-prompts.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/resource-prompts.bench.js +181 -0
- package/dist/__tests__/benchmarks/resource-prompts.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts +11 -0
- package/dist/__tests__/benchmarks/schema-parsing.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/schema-parsing.bench.js +209 -0
- package/dist/__tests__/benchmarks/schema-parsing.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts +9 -0
- package/dist/__tests__/benchmarks/tool-filtering.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/tool-filtering.bench.js +83 -0
- package/dist/__tests__/benchmarks/tool-filtering.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/transport-auth.bench.d.ts +10 -0
- package/dist/__tests__/benchmarks/transport-auth.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/transport-auth.bench.js +128 -0
- package/dist/__tests__/benchmarks/transport-auth.bench.js.map +1 -0
- package/dist/__tests__/benchmarks/utilities.bench.d.ts +10 -0
- package/dist/__tests__/benchmarks/utilities.bench.d.ts.map +1 -0
- package/dist/__tests__/benchmarks/utilities.bench.js +164 -0
- package/dist/__tests__/benchmarks/utilities.bench.js.map +1 -0
- package/dist/adapters/DatabaseAdapter.d.ts.map +1 -1
- package/dist/adapters/DatabaseAdapter.js +12 -0
- package/dist/adapters/DatabaseAdapter.js.map +1 -1
- package/dist/adapters/postgresql/PostgresAdapter.d.ts.map +1 -1
- package/dist/adapters/postgresql/PostgresAdapter.js +56 -3
- package/dist/adapters/postgresql/PostgresAdapter.js.map +1 -1
- package/dist/adapters/postgresql/prompts/ltree.js +2 -2
- package/dist/adapters/postgresql/prompts/ltree.js.map +1 -1
- package/dist/adapters/postgresql/schemas/admin.d.ts +10 -5
- package/dist/adapters/postgresql/schemas/admin.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/admin.js +10 -5
- package/dist/adapters/postgresql/schemas/admin.js.map +1 -1
- package/dist/adapters/postgresql/schemas/backup.d.ts +45 -27
- package/dist/adapters/postgresql/schemas/backup.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/backup.js +64 -26
- package/dist/adapters/postgresql/schemas/backup.js.map +1 -1
- package/dist/adapters/postgresql/schemas/core.d.ts +53 -19
- package/dist/adapters/postgresql/schemas/core.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/core.js +61 -17
- package/dist/adapters/postgresql/schemas/core.js.map +1 -1
- package/dist/adapters/postgresql/schemas/cron.d.ts +51 -32
- package/dist/adapters/postgresql/schemas/cron.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/cron.js +64 -44
- package/dist/adapters/postgresql/schemas/cron.js.map +1 -1
- package/dist/adapters/postgresql/schemas/extensions.d.ts +224 -110
- package/dist/adapters/postgresql/schemas/extensions.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/extensions.js +245 -96
- package/dist/adapters/postgresql/schemas/extensions.js.map +1 -1
- package/dist/adapters/postgresql/schemas/index.d.ts +7 -6
- package/dist/adapters/postgresql/schemas/index.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/index.js +16 -8
- package/dist/adapters/postgresql/schemas/index.js.map +1 -1
- package/dist/adapters/postgresql/schemas/introspection.d.ts +445 -0
- package/dist/adapters/postgresql/schemas/introspection.d.ts.map +1 -0
- package/dist/adapters/postgresql/schemas/introspection.js +478 -0
- package/dist/adapters/postgresql/schemas/introspection.js.map +1 -0
- package/dist/adapters/postgresql/schemas/jsonb.d.ts +102 -42
- package/dist/adapters/postgresql/schemas/jsonb.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/jsonb.js +125 -30
- package/dist/adapters/postgresql/schemas/jsonb.js.map +1 -1
- package/dist/adapters/postgresql/schemas/monitoring.d.ts +69 -36
- package/dist/adapters/postgresql/schemas/monitoring.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/monitoring.js +98 -40
- package/dist/adapters/postgresql/schemas/monitoring.js.map +1 -1
- package/dist/adapters/postgresql/schemas/partitioning.d.ts +21 -24
- package/dist/adapters/postgresql/schemas/partitioning.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/partitioning.js +26 -14
- package/dist/adapters/postgresql/schemas/partitioning.js.map +1 -1
- package/dist/adapters/postgresql/schemas/partman.d.ts +69 -0
- package/dist/adapters/postgresql/schemas/partman.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/partman.js +46 -33
- package/dist/adapters/postgresql/schemas/partman.js.map +1 -1
- package/dist/adapters/postgresql/schemas/performance.d.ts +97 -49
- package/dist/adapters/postgresql/schemas/performance.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/performance.js +139 -34
- package/dist/adapters/postgresql/schemas/performance.js.map +1 -1
- package/dist/adapters/postgresql/schemas/postgis.d.ts +20 -0
- package/dist/adapters/postgresql/schemas/postgis.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/postgis.js +40 -0
- package/dist/adapters/postgresql/schemas/postgis.js.map +1 -1
- package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts +50 -30
- package/dist/adapters/postgresql/schemas/schema-mgmt.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/schema-mgmt.js +105 -33
- package/dist/adapters/postgresql/schemas/schema-mgmt.js.map +1 -1
- package/dist/adapters/postgresql/schemas/stats.d.ts +33 -20
- package/dist/adapters/postgresql/schemas/stats.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/stats.js +36 -20
- package/dist/adapters/postgresql/schemas/stats.js.map +1 -1
- package/dist/adapters/postgresql/schemas/text-search.d.ts +34 -19
- package/dist/adapters/postgresql/schemas/text-search.d.ts.map +1 -1
- package/dist/adapters/postgresql/schemas/text-search.js +52 -13
- package/dist/adapters/postgresql/schemas/text-search.js.map +1 -1
- package/dist/adapters/postgresql/tools/admin.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/admin.js +272 -186
- package/dist/adapters/postgresql/tools/admin.js.map +1 -1
- package/dist/adapters/postgresql/tools/backup/dump.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/backup/dump.js +376 -350
- package/dist/adapters/postgresql/tools/backup/dump.js.map +1 -1
- package/dist/adapters/postgresql/tools/citext.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/citext.js +333 -243
- package/dist/adapters/postgresql/tools/citext.js.map +1 -1
- package/dist/adapters/postgresql/tools/codemode/index.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/codemode/index.js +2 -11
- package/dist/adapters/postgresql/tools/codemode/index.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/convenience.d.ts +9 -1
- package/dist/adapters/postgresql/tools/core/convenience.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/convenience.js +101 -19
- package/dist/adapters/postgresql/tools/core/convenience.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/error-helpers.d.ts +48 -0
- package/dist/adapters/postgresql/tools/core/error-helpers.d.ts.map +1 -0
- package/dist/adapters/postgresql/tools/core/error-helpers.js +256 -0
- package/dist/adapters/postgresql/tools/core/error-helpers.js.map +1 -0
- package/dist/adapters/postgresql/tools/core/health.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/health.js +18 -4
- package/dist/adapters/postgresql/tools/core/health.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/indexes.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/indexes.js +48 -6
- package/dist/adapters/postgresql/tools/core/indexes.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/objects.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/objects.js +104 -85
- package/dist/adapters/postgresql/tools/core/objects.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/query.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/query.js +100 -42
- package/dist/adapters/postgresql/tools/core/query.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/schemas.d.ts +51 -25
- package/dist/adapters/postgresql/tools/core/schemas.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/schemas.js +51 -25
- package/dist/adapters/postgresql/tools/core/schemas.js.map +1 -1
- package/dist/adapters/postgresql/tools/core/tables.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/core/tables.js +72 -32
- package/dist/adapters/postgresql/tools/core/tables.js.map +1 -1
- package/dist/adapters/postgresql/tools/cron.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/cron.js +333 -206
- package/dist/adapters/postgresql/tools/cron.js.map +1 -1
- package/dist/adapters/postgresql/tools/introspection.d.ts +15 -0
- package/dist/adapters/postgresql/tools/introspection.d.ts.map +1 -0
- package/dist/adapters/postgresql/tools/introspection.js +1682 -0
- package/dist/adapters/postgresql/tools/introspection.js.map +1 -0
- package/dist/adapters/postgresql/tools/jsonb/advanced.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/jsonb/advanced.js +394 -297
- package/dist/adapters/postgresql/tools/jsonb/advanced.js.map +1 -1
- package/dist/adapters/postgresql/tools/jsonb/basic.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/jsonb/basic.js +686 -398
- package/dist/adapters/postgresql/tools/jsonb/basic.js.map +1 -1
- package/dist/adapters/postgresql/tools/kcache.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/kcache.js +278 -246
- package/dist/adapters/postgresql/tools/kcache.js.map +1 -1
- package/dist/adapters/postgresql/tools/ltree.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/ltree.js +137 -38
- package/dist/adapters/postgresql/tools/ltree.js.map +1 -1
- package/dist/adapters/postgresql/tools/monitoring.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/monitoring.js +86 -55
- package/dist/adapters/postgresql/tools/monitoring.js.map +1 -1
- package/dist/adapters/postgresql/tools/partitioning.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/partitioning.js +79 -15
- package/dist/adapters/postgresql/tools/partitioning.js.map +1 -1
- package/dist/adapters/postgresql/tools/partman/management.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/partman/management.js +43 -56
- package/dist/adapters/postgresql/tools/partman/management.js.map +1 -1
- package/dist/adapters/postgresql/tools/partman/operations.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/partman/operations.js +137 -24
- package/dist/adapters/postgresql/tools/partman/operations.js.map +1 -1
- package/dist/adapters/postgresql/tools/performance/analysis.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/performance/analysis.js +276 -165
- package/dist/adapters/postgresql/tools/performance/analysis.js.map +1 -1
- package/dist/adapters/postgresql/tools/performance/explain.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/performance/explain.js +61 -21
- package/dist/adapters/postgresql/tools/performance/explain.js.map +1 -1
- package/dist/adapters/postgresql/tools/performance/monitoring.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/performance/monitoring.js +52 -12
- package/dist/adapters/postgresql/tools/performance/monitoring.js.map +1 -1
- package/dist/adapters/postgresql/tools/performance/optimization.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/performance/optimization.js +92 -81
- package/dist/adapters/postgresql/tools/performance/optimization.js.map +1 -1
- package/dist/adapters/postgresql/tools/performance/stats.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/performance/stats.js +182 -60
- package/dist/adapters/postgresql/tools/performance/stats.js.map +1 -1
- package/dist/adapters/postgresql/tools/pgcrypto.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/pgcrypto.js +277 -102
- package/dist/adapters/postgresql/tools/pgcrypto.js.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/advanced.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/advanced.js +298 -230
- package/dist/adapters/postgresql/tools/postgis/advanced.js.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/basic.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/basic.js +370 -251
- package/dist/adapters/postgresql/tools/postgis/basic.js.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/standalone.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/postgis/standalone.js +135 -51
- package/dist/adapters/postgresql/tools/postgis/standalone.js.map +1 -1
- package/dist/adapters/postgresql/tools/schema.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/schema.js +580 -233
- package/dist/adapters/postgresql/tools/schema.js.map +1 -1
- package/dist/adapters/postgresql/tools/stats/advanced.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/stats/advanced.js +567 -506
- package/dist/adapters/postgresql/tools/stats/advanced.js.map +1 -1
- package/dist/adapters/postgresql/tools/stats/basic.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/stats/basic.js +340 -316
- package/dist/adapters/postgresql/tools/stats/basic.js.map +1 -1
- package/dist/adapters/postgresql/tools/text.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/text.js +690 -337
- package/dist/adapters/postgresql/tools/text.js.map +1 -1
- package/dist/adapters/postgresql/tools/transactions.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/transactions.js +157 -50
- package/dist/adapters/postgresql/tools/transactions.js.map +1 -1
- package/dist/adapters/postgresql/tools/vector/advanced.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/vector/advanced.js +18 -0
- package/dist/adapters/postgresql/tools/vector/advanced.js.map +1 -1
- package/dist/adapters/postgresql/tools/vector/basic.d.ts.map +1 -1
- package/dist/adapters/postgresql/tools/vector/basic.js +100 -53
- package/dist/adapters/postgresql/tools/vector/basic.js.map +1 -1
- package/dist/auth/auth-context.d.ts +28 -0
- package/dist/auth/auth-context.d.ts.map +1 -0
- package/dist/auth/auth-context.js +37 -0
- package/dist/auth/auth-context.js.map +1 -0
- package/dist/auth/scope-map.d.ts +20 -0
- package/dist/auth/scope-map.d.ts.map +1 -0
- package/dist/auth/scope-map.js +40 -0
- package/dist/auth/scope-map.js.map +1 -0
- package/dist/auth/scopes.d.ts.map +1 -1
- package/dist/auth/scopes.js +2 -0
- package/dist/auth/scopes.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/codemode/api.d.ts +1 -0
- package/dist/codemode/api.d.ts.map +1 -1
- package/dist/codemode/api.js +35 -1
- package/dist/codemode/api.js.map +1 -1
- package/dist/codemode/index.d.ts +0 -2
- package/dist/codemode/index.d.ts.map +1 -1
- package/dist/codemode/index.js +0 -4
- package/dist/codemode/index.js.map +1 -1
- package/dist/codemode/sandbox.d.ts +14 -1
- package/dist/codemode/sandbox.d.ts.map +1 -1
- package/dist/codemode/sandbox.js +58 -19
- package/dist/codemode/sandbox.js.map +1 -1
- package/dist/codemode/types.d.ts.map +1 -1
- package/dist/codemode/types.js +3 -0
- package/dist/codemode/types.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +5 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +117 -31
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/filtering/ToolConstants.d.ts +22 -19
- package/dist/filtering/ToolConstants.d.ts.map +1 -1
- package/dist/filtering/ToolConstants.js +48 -37
- package/dist/filtering/ToolConstants.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +10 -13
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/pool/ConnectionPool.js +1 -1
- package/dist/pool/ConnectionPool.js.map +1 -1
- package/dist/transports/http.d.ts +1 -0
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +75 -21
- package/dist/transports/http.js.map +1 -1
- package/dist/types/filtering.d.ts +2 -2
- package/dist/types/filtering.d.ts.map +1 -1
- package/dist/utils/icons.d.ts.map +1 -1
- package/dist/utils/icons.js +5 -0
- package/dist/utils/icons.js.map +1 -1
- package/dist/utils/where-clause.d.ts.map +1 -1
- package/dist/utils/where-clause.js +24 -0
- package/dist/utils/where-clause.js.map +1 -1
- package/package.json +20 -13
- package/dist/codemode/sandbox-factory.d.ts +0 -72
- package/dist/codemode/sandbox-factory.d.ts.map +0 -1
- package/dist/codemode/sandbox-factory.js +0 -88
- package/dist/codemode/sandbox-factory.js.map +0 -1
- package/dist/codemode/worker-sandbox.d.ts +0 -82
- package/dist/codemode/worker-sandbox.d.ts.map +0 -1
- package/dist/codemode/worker-sandbox.js +0 -244
- package/dist/codemode/worker-sandbox.js.map +0 -1
- package/dist/codemode/worker-script.d.ts +0 -8
- package/dist/codemode/worker-script.d.ts.map +0 -1
- package/dist/codemode/worker-script.js +0 -113
- 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 =
|
|
188
|
-
AND table_name =
|
|
189
|
-
AND column_name =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
SELECT COUNT(DISTINCT "${groupBy}")
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
?
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
:
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
//
|
|
604
|
-
const
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
"
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
763
|
-
groups,
|
|
764
|
-
count: groups.length,
|
|
841
|
+
results: testResults,
|
|
765
842
|
};
|
|
766
843
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
854
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
}
|