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