@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
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Advanced spatial tools: geocode, geo_transform, geo_index_optimize, geo_cluster.
|
|
5
5
|
*/
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
import { z, ZodError } from "zod";
|
|
7
7
|
import { readOnly } from "../../../../utils/annotations.js";
|
|
8
8
|
import { getToolIcons } from "../../../../utils/icons.js";
|
|
9
|
+
import { formatPostgresError } from "../core/error-helpers.js";
|
|
10
|
+
import { sanitizeIdentifier, sanitizeTableName, } from "../../../../utils/identifiers.js";
|
|
11
|
+
import { sanitizeWhereClause } from "../../../../utils/where-clause.js";
|
|
9
12
|
import { GeocodeSchemaBase, GeocodeSchema, GeoTransformSchemaBase, GeoTransformSchema, GeoClusterSchemaBase, GeoClusterSchema,
|
|
10
13
|
// Output schemas
|
|
11
14
|
GeocodeOutputSchema, GeoTransformOutputSchema, GeoIndexOptimizeOutputSchema, GeoClusterOutputSchema, } from "../../schemas/index.js";
|
|
@@ -19,27 +22,41 @@ export function createGeocodeTool(adapter) {
|
|
|
19
22
|
annotations: readOnly("Geocode"),
|
|
20
23
|
icons: getToolIcons("postgis", readOnly("Geocode")),
|
|
21
24
|
handler: async (params, _context) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const parsed = GeocodeSchema.parse(params ?? {});
|
|
27
|
+
const srid = parsed.srid ?? 4326;
|
|
28
|
+
const sql = `SELECT
|
|
25
29
|
ST_AsGeoJSON(ST_SetSRID(ST_MakePoint($1, $2), $3)) as geojson,
|
|
26
30
|
ST_AsText(ST_SetSRID(ST_MakePoint($1, $2), $3)) as wkt`;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const result = await adapter.executeQuery(sql, [
|
|
32
|
+
parsed.lng,
|
|
33
|
+
parsed.lat,
|
|
34
|
+
srid,
|
|
35
|
+
]);
|
|
36
|
+
// Add note about SRID for non-4326 cases
|
|
37
|
+
const row = result.rows?.[0];
|
|
38
|
+
if (row === undefined) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
const response = { ...row };
|
|
42
|
+
if (srid !== 4326) {
|
|
43
|
+
response["note"] =
|
|
44
|
+
`Coordinates are WGS84 lat/lng with SRID ${String(srid)} metadata. Use pg_geo_transform to convert to target CRS.`;
|
|
45
|
+
}
|
|
46
|
+
return response;
|
|
36
47
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (error instanceof ZodError) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: error.issues.map((i) => i.message).join("; "),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: formatPostgresError(error, { tool: "pg_geocode" }),
|
|
58
|
+
};
|
|
41
59
|
}
|
|
42
|
-
return response;
|
|
43
60
|
},
|
|
44
61
|
};
|
|
45
62
|
}
|
|
@@ -56,84 +73,114 @@ export function createGeoTransformTool(adapter) {
|
|
|
56
73
|
annotations: readOnly("Transform Geometry"),
|
|
57
74
|
icons: getToolIcons("postgis", readOnly("Transform Geometry")),
|
|
58
75
|
handler: async (params, _context) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
try {
|
|
77
|
+
const parsed = GeoTransformSchema.parse(params ?? {});
|
|
78
|
+
const schemaName = parsed.schema ?? "public";
|
|
79
|
+
const qualifiedTable = sanitizeTableName(parsed.table, schemaName !== "public" ? schemaName : undefined);
|
|
80
|
+
const columnName = sanitizeIdentifier(parsed.column);
|
|
81
|
+
// Auto-detect fromSrid from column metadata if not provided
|
|
82
|
+
let fromSrid = parsed.fromSrid;
|
|
83
|
+
if (fromSrid === 0) {
|
|
84
|
+
// Check if table exists before attempting SRID auto-detection
|
|
85
|
+
const tableCheckSql = `SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2`;
|
|
86
|
+
const tableCheckResult = await adapter.executeQuery(tableCheckSql, [
|
|
87
|
+
schemaName,
|
|
88
|
+
parsed.table,
|
|
89
|
+
]);
|
|
90
|
+
if ((tableCheckResult.rows?.length ?? 0) === 0) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: `Table or view '${parsed.table}' not found. Use pg_list_tables to see available tables.`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const sridQuery = `
|
|
97
|
+
SELECT srid FROM geometry_columns
|
|
98
|
+
WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geometry_column = $3
|
|
99
|
+
UNION
|
|
100
|
+
SELECT srid FROM geography_columns
|
|
101
|
+
WHERE f_table_schema = $1 AND f_table_name = $2 AND f_geography_column = $3
|
|
102
|
+
LIMIT 1
|
|
103
|
+
`;
|
|
104
|
+
const sridResult = await adapter.executeQuery(sridQuery, [
|
|
105
|
+
schemaName,
|
|
106
|
+
parsed.table,
|
|
107
|
+
parsed.column,
|
|
108
|
+
]);
|
|
109
|
+
const sridValue = sridResult.rows?.[0]?.["srid"];
|
|
110
|
+
if (sridValue !== undefined && sridValue !== null) {
|
|
111
|
+
fromSrid = Number(sridValue);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: `Could not auto-detect SRID for column "${parsed.column}" on table "${parsed.table}". Provide fromSrid (or sourceSrid) explicitly.`,
|
|
117
|
+
suggestion: `Use fromSrid: 4326 for WGS84/GPS coordinates, or fromSrid: 3857 for Web Mercator`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const whereClause = parsed.where !== undefined
|
|
122
|
+
? `WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
123
|
+
: "";
|
|
124
|
+
// Default limit of 50 to prevent large payloads, use limit: 0 for all
|
|
125
|
+
const effectiveLimit = parsed.limit ?? 50;
|
|
126
|
+
const limitClause = effectiveLimit > 0 ? `LIMIT ${String(effectiveLimit)}` : "";
|
|
127
|
+
// Get non-geometry columns to avoid returning raw WKB
|
|
128
|
+
const colQuery = `
|
|
129
|
+
SELECT column_name FROM information_schema.columns
|
|
130
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
131
|
+
AND udt_name NOT IN ('geometry', 'geography')
|
|
132
|
+
ORDER BY ordinal_position
|
|
75
133
|
`;
|
|
76
|
-
const
|
|
134
|
+
const colResult = await adapter.executeQuery(colQuery, [
|
|
77
135
|
schemaName,
|
|
78
136
|
parsed.table,
|
|
79
|
-
parsed.column,
|
|
80
137
|
]);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
const nonGeomCols = (colResult.rows ?? [])
|
|
139
|
+
.map((row) => `"${String(row["column_name"])}"`)
|
|
140
|
+
.join(", ");
|
|
141
|
+
// Select non-geometry columns + transformed geometry representations
|
|
142
|
+
const selectCols = nonGeomCols.length > 0
|
|
143
|
+
? `${nonGeomCols}, ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`
|
|
144
|
+
: `ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`;
|
|
145
|
+
const sql = `SELECT ${selectCols} FROM ${qualifiedTable} ${whereClause} ${limitClause}`;
|
|
146
|
+
const result = await adapter.executeQuery(sql);
|
|
147
|
+
// Build response with truncation indicators if default limit was applied
|
|
148
|
+
const response = {
|
|
149
|
+
results: result.rows,
|
|
150
|
+
count: result.rows?.length ?? 0,
|
|
151
|
+
fromSrid: fromSrid,
|
|
152
|
+
toSrid: parsed.toSrid,
|
|
153
|
+
...(parsed.fromSrid === 0 && { autoDetectedSrid: true }),
|
|
154
|
+
};
|
|
155
|
+
// Check if results were truncated (works for both default and explicit limits)
|
|
156
|
+
if (effectiveLimit > 0) {
|
|
157
|
+
const countSql = `SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`;
|
|
158
|
+
const countResult = await adapter.executeQuery(countSql);
|
|
159
|
+
const totalCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
|
|
160
|
+
if (totalCount > effectiveLimit) {
|
|
161
|
+
response["truncated"] = true;
|
|
162
|
+
response["totalCount"] = totalCount;
|
|
163
|
+
response["limit"] = effectiveLimit;
|
|
164
|
+
}
|
|
84
165
|
}
|
|
85
|
-
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof ZodError) {
|
|
86
170
|
return {
|
|
87
171
|
success: false,
|
|
88
|
-
error:
|
|
89
|
-
suggestion: `Use fromSrid: 4326 for WGS84/GPS coordinates, or fromSrid: 3857 for Web Mercator`,
|
|
172
|
+
error: error.issues.map((i) => i.message).join("; "),
|
|
90
173
|
};
|
|
91
174
|
}
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: formatPostgresError(error, {
|
|
178
|
+
tool: "pg_geo_transform",
|
|
179
|
+
table: params?.["table"] ??
|
|
180
|
+
undefined,
|
|
181
|
+
}),
|
|
182
|
+
};
|
|
92
183
|
}
|
|
93
|
-
const whereClause = parsed.where !== undefined ? `WHERE ${parsed.where}` : "";
|
|
94
|
-
// Default limit of 50 to prevent large payloads, use limit: 0 for all
|
|
95
|
-
const effectiveLimit = parsed.limit ?? 50;
|
|
96
|
-
const limitClause = effectiveLimit > 0 ? `LIMIT ${String(effectiveLimit)}` : "";
|
|
97
|
-
// Get non-geometry columns to avoid returning raw WKB
|
|
98
|
-
const colQuery = `
|
|
99
|
-
SELECT column_name FROM information_schema.columns
|
|
100
|
-
WHERE table_schema = $1 AND table_name = $2
|
|
101
|
-
AND udt_name NOT IN ('geometry', 'geography')
|
|
102
|
-
ORDER BY ordinal_position
|
|
103
|
-
`;
|
|
104
|
-
const colResult = await adapter.executeQuery(colQuery, [
|
|
105
|
-
schemaName,
|
|
106
|
-
parsed.table,
|
|
107
|
-
]);
|
|
108
|
-
const nonGeomCols = (colResult.rows ?? [])
|
|
109
|
-
.map((row) => `"${String(row["column_name"])}"`)
|
|
110
|
-
.join(", ");
|
|
111
|
-
// Select non-geometry columns + transformed geometry representations
|
|
112
|
-
const selectCols = nonGeomCols.length > 0
|
|
113
|
-
? `${nonGeomCols}, ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`
|
|
114
|
-
: `ST_AsGeoJSON(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_geojson, ST_AsText(ST_Transform(ST_SetSRID(${columnName}, ${String(fromSrid)}), ${String(parsed.toSrid)})) as transformed_wkt, ${String(parsed.toSrid)} as output_srid`;
|
|
115
|
-
const sql = `SELECT ${selectCols} FROM ${qualifiedTable} ${whereClause} ${limitClause}`;
|
|
116
|
-
const result = await adapter.executeQuery(sql);
|
|
117
|
-
// Build response with truncation indicators if default limit was applied
|
|
118
|
-
const response = {
|
|
119
|
-
results: result.rows,
|
|
120
|
-
count: result.rows?.length ?? 0,
|
|
121
|
-
fromSrid: fromSrid,
|
|
122
|
-
toSrid: parsed.toSrid,
|
|
123
|
-
...(parsed.fromSrid === 0 && { autoDetectedSrid: true }),
|
|
124
|
-
};
|
|
125
|
-
// Check if results were truncated (works for both default and explicit limits)
|
|
126
|
-
if (effectiveLimit > 0) {
|
|
127
|
-
const countSql = `SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`;
|
|
128
|
-
const countResult = await adapter.executeQuery(countSql);
|
|
129
|
-
const totalCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
|
|
130
|
-
if (totalCount > effectiveLimit) {
|
|
131
|
-
response["truncated"] = true;
|
|
132
|
-
response["totalCount"] = totalCount;
|
|
133
|
-
response["limit"] = effectiveLimit;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return response;
|
|
137
184
|
},
|
|
138
185
|
};
|
|
139
186
|
}
|
|
@@ -159,7 +206,7 @@ export function createGeoIndexOptimizeTool(adapter) {
|
|
|
159
206
|
const parsed = params;
|
|
160
207
|
const schemaName = parsed.schema ?? "public";
|
|
161
208
|
const indexQuery = `
|
|
162
|
-
SELECT
|
|
209
|
+
SELECT
|
|
163
210
|
c.relname as table_name,
|
|
164
211
|
i.relname as index_name,
|
|
165
212
|
a.attname as column_name,
|
|
@@ -178,13 +225,16 @@ export function createGeoIndexOptimizeTool(adapter) {
|
|
|
178
225
|
WHERE n.nspname = $1
|
|
179
226
|
AND (pg_get_indexdef(i.oid) LIKE '%gist%' OR pg_get_indexdef(i.oid) LIKE '%spgist%')
|
|
180
227
|
AND t.typname IN ('geometry', 'geography')
|
|
181
|
-
${parsed.table !== undefined ? `AND c.relname =
|
|
228
|
+
${parsed.table !== undefined ? `AND c.relname = $2` : ""}
|
|
182
229
|
ORDER BY index_size_bytes DESC
|
|
183
230
|
`;
|
|
231
|
+
const indexParams = [schemaName];
|
|
232
|
+
if (parsed.table !== undefined)
|
|
233
|
+
indexParams.push(parsed.table);
|
|
184
234
|
const [indexes, tableStats] = await Promise.all([
|
|
185
|
-
adapter.executeQuery(indexQuery,
|
|
235
|
+
adapter.executeQuery(indexQuery, indexParams),
|
|
186
236
|
adapter.executeQuery(`
|
|
187
|
-
SELECT
|
|
237
|
+
SELECT
|
|
188
238
|
c.relname as table_name,
|
|
189
239
|
n_live_tup as row_count,
|
|
190
240
|
pg_size_pretty(pg_table_size(c.oid)) as table_size
|
|
@@ -192,14 +242,14 @@ export function createGeoIndexOptimizeTool(adapter) {
|
|
|
192
242
|
JOIN pg_class c ON c.relname = t.relname
|
|
193
243
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
194
244
|
WHERE n.nspname = $1
|
|
195
|
-
${parsed.table !== undefined ? `AND c.relname =
|
|
245
|
+
${parsed.table !== undefined ? `AND c.relname = $${String(parsed.table !== undefined ? 2 : 1)}` : ""}
|
|
196
246
|
AND EXISTS (
|
|
197
247
|
SELECT 1 FROM information_schema.columns ic
|
|
198
|
-
WHERE ic.table_schema = n.nspname
|
|
199
|
-
AND ic.table_name = c.relname
|
|
248
|
+
WHERE ic.table_schema = n.nspname
|
|
249
|
+
AND ic.table_name = c.relname
|
|
200
250
|
AND ic.udt_name IN ('geometry', 'geography')
|
|
201
251
|
)
|
|
202
|
-
`,
|
|
252
|
+
`, indexParams),
|
|
203
253
|
]);
|
|
204
254
|
const recommendations = [];
|
|
205
255
|
for (const idx of indexes.rows ?? []) {
|
|
@@ -267,151 +317,169 @@ export function createGeoClusterTool(adapter) {
|
|
|
267
317
|
annotations: readOnly("Geo Cluster"),
|
|
268
318
|
icons: getToolIcons("postgis", readOnly("Geo Cluster")),
|
|
269
319
|
handler: async (params, _context) => {
|
|
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
|
-
|
|
320
|
+
try {
|
|
321
|
+
const parsed = GeoClusterSchema.parse(params ?? {});
|
|
322
|
+
const method = parsed.method ?? "dbscan";
|
|
323
|
+
const schemaName = parsed.schema ?? "public";
|
|
324
|
+
const qualifiedTable = sanitizeTableName(parsed.table, schemaName !== "public" ? schemaName : undefined);
|
|
325
|
+
const whereClause = parsed.where !== undefined
|
|
326
|
+
? `WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
327
|
+
: "";
|
|
328
|
+
const limitClause = parsed.limit !== undefined && parsed.limit > 0
|
|
329
|
+
? `LIMIT ${String(parsed.limit)}`
|
|
330
|
+
: "";
|
|
331
|
+
// Track warning if K > N
|
|
332
|
+
let warning;
|
|
333
|
+
// For K-Means, validate and adjust numClusters
|
|
334
|
+
let effectiveNumClusters = parsed.numClusters ?? 5;
|
|
335
|
+
let rowCount;
|
|
336
|
+
if (method === "kmeans") {
|
|
337
|
+
// Validate numClusters > 0
|
|
338
|
+
if (effectiveNumClusters <= 0) {
|
|
339
|
+
return {
|
|
340
|
+
error: `numClusters must be greater than 0 (received: ${String(effectiveNumClusters)}).`,
|
|
341
|
+
method,
|
|
342
|
+
table: parsed.table,
|
|
343
|
+
numClusters: effectiveNumClusters,
|
|
344
|
+
suggestion: "Provide a positive integer for numClusters (e.g., numClusters: 3)",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const countResult = await adapter.executeQuery(`SELECT COUNT(*) as cnt FROM ${qualifiedTable} ${whereClause}`);
|
|
348
|
+
rowCount = Number(countResult.rows?.[0]?.["cnt"] ?? 0);
|
|
349
|
+
if (rowCount === 0) {
|
|
350
|
+
return {
|
|
351
|
+
error: `No rows found in table ${parsed.table}${whereClause !== "" ? " matching filter" : ""}. K-Means requires at least 1 row.`,
|
|
352
|
+
method,
|
|
353
|
+
table: parsed.table,
|
|
354
|
+
rowCount: 0,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// Clamp K to row count and warn if exceeded
|
|
358
|
+
if (effectiveNumClusters > rowCount) {
|
|
359
|
+
warning = `Requested ${String(parsed.numClusters)} clusters but only ${String(rowCount)} rows available. Using ${String(rowCount)} clusters instead.`;
|
|
360
|
+
effectiveNumClusters = rowCount;
|
|
361
|
+
}
|
|
295
362
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return {
|
|
300
|
-
error: `No rows found in table ${parsed.table}${whereClause !== "" ? " matching filter" : ""}. K-Means requires at least 1 row.`,
|
|
301
|
-
method,
|
|
302
|
-
table: parsed.table,
|
|
303
|
-
rowCount: 0,
|
|
304
|
-
};
|
|
363
|
+
let clusterFunction;
|
|
364
|
+
if (method === "kmeans") {
|
|
365
|
+
clusterFunction = `ST_ClusterKMeans("${parsed.column}", ${String(effectiveNumClusters)}) OVER ()`;
|
|
305
366
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
367
|
+
else {
|
|
368
|
+
const eps = parsed.eps ?? 100;
|
|
369
|
+
const minPoints = parsed.minPoints ?? 3;
|
|
370
|
+
clusterFunction = `ST_ClusterDBSCAN("${parsed.column}", ${String(eps)}, ${String(minPoints)}) OVER ()`;
|
|
310
371
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
? { numClusters: effectiveNumClusters }
|
|
372
|
-
: { eps: parsed.eps ?? 100, minPoints: parsed.minPoints ?? 3 },
|
|
373
|
-
summary: normalizedSummary,
|
|
374
|
-
clusters: normalizedClusters,
|
|
375
|
-
};
|
|
376
|
-
// Add warning if K was clamped
|
|
377
|
-
if (warning !== undefined) {
|
|
378
|
-
response["warning"] = warning;
|
|
379
|
-
response["requestedClusters"] = parsed.numClusters;
|
|
380
|
-
response["actualClusters"] = effectiveNumClusters;
|
|
381
|
-
}
|
|
382
|
-
// Add contextual hints based on method and results
|
|
383
|
-
const numClusters = normalizedSummary.num_clusters;
|
|
384
|
-
const noisePoints = normalizedSummary.noise_points;
|
|
385
|
-
const totalPoints = normalizedSummary.total_points;
|
|
386
|
-
if (method === "dbscan") {
|
|
387
|
-
const eps = parsed.eps ?? 100;
|
|
388
|
-
const minPoints = parsed.minPoints ?? 3;
|
|
389
|
-
// Provide hints about DBSCAN parameter trade-offs
|
|
390
|
-
const hints = [];
|
|
391
|
-
if (numClusters === 1 && totalPoints > 1) {
|
|
392
|
-
hints.push(`All ${String(totalPoints)} points formed a single cluster. Consider decreasing eps (currently ${String(eps)}m) to create more distinct clusters.`);
|
|
372
|
+
const sql = `
|
|
373
|
+
WITH clustered AS (
|
|
374
|
+
SELECT
|
|
375
|
+
*,
|
|
376
|
+
${clusterFunction} as cluster_id
|
|
377
|
+
FROM ${qualifiedTable}
|
|
378
|
+
${whereClause}
|
|
379
|
+
)
|
|
380
|
+
SELECT
|
|
381
|
+
cluster_id,
|
|
382
|
+
COUNT(*) as point_count,
|
|
383
|
+
ST_AsGeoJSON(ST_Centroid(ST_Collect("${parsed.column}"))) as centroid,
|
|
384
|
+
ST_AsGeoJSON(ST_ConvexHull(ST_Collect("${parsed.column}"))) as hull
|
|
385
|
+
FROM clustered
|
|
386
|
+
WHERE cluster_id IS NOT NULL
|
|
387
|
+
GROUP BY cluster_id
|
|
388
|
+
ORDER BY point_count DESC
|
|
389
|
+
${limitClause}
|
|
390
|
+
`;
|
|
391
|
+
const [clustersResult, summaryResult] = await Promise.all([
|
|
392
|
+
adapter.executeQuery(sql),
|
|
393
|
+
adapter.executeQuery(`
|
|
394
|
+
WITH clustered AS (
|
|
395
|
+
SELECT ${clusterFunction} as cluster_id
|
|
396
|
+
FROM ${qualifiedTable}
|
|
397
|
+
${whereClause}
|
|
398
|
+
)
|
|
399
|
+
SELECT
|
|
400
|
+
COUNT(DISTINCT cluster_id) as num_clusters,
|
|
401
|
+
COUNT(*) FILTER (WHERE cluster_id IS NULL) as noise_points,
|
|
402
|
+
COUNT(*) as total_points
|
|
403
|
+
FROM clustered
|
|
404
|
+
`),
|
|
405
|
+
]);
|
|
406
|
+
// Normalize cluster point_count to numbers
|
|
407
|
+
const normalizedClusters = (clustersResult.rows ?? []).map((row) => ({
|
|
408
|
+
...row,
|
|
409
|
+
point_count: Number(row["point_count"]),
|
|
410
|
+
}));
|
|
411
|
+
// Normalize summary values to numbers for consistency
|
|
412
|
+
const rawSummary = summaryResult.rows?.[0] ?? {};
|
|
413
|
+
const normalizedSummary = {
|
|
414
|
+
num_clusters: Number(rawSummary["num_clusters"] ?? 0),
|
|
415
|
+
noise_points: Number(rawSummary["noise_points"] ?? 0),
|
|
416
|
+
total_points: Number(rawSummary["total_points"] ?? 0),
|
|
417
|
+
};
|
|
418
|
+
// Build response
|
|
419
|
+
const response = {
|
|
420
|
+
method,
|
|
421
|
+
parameters: method === "kmeans"
|
|
422
|
+
? { numClusters: effectiveNumClusters }
|
|
423
|
+
: { eps: parsed.eps ?? 100, minPoints: parsed.minPoints ?? 3 },
|
|
424
|
+
summary: normalizedSummary,
|
|
425
|
+
clusters: normalizedClusters,
|
|
426
|
+
};
|
|
427
|
+
// Add warning if K was clamped
|
|
428
|
+
if (warning !== undefined) {
|
|
429
|
+
response["warning"] = warning;
|
|
430
|
+
response["requestedClusters"] = parsed.numClusters;
|
|
431
|
+
response["actualClusters"] = effectiveNumClusters;
|
|
393
432
|
}
|
|
394
|
-
|
|
395
|
-
|
|
433
|
+
// Add contextual hints based on method and results
|
|
434
|
+
const numClusters = normalizedSummary.num_clusters;
|
|
435
|
+
const noisePoints = normalizedSummary.noise_points;
|
|
436
|
+
const totalPoints = normalizedSummary.total_points;
|
|
437
|
+
if (method === "dbscan") {
|
|
438
|
+
const eps = parsed.eps ?? 100;
|
|
439
|
+
const minPoints = parsed.minPoints ?? 3;
|
|
440
|
+
// Provide hints about DBSCAN parameter trade-offs
|
|
441
|
+
const hints = [];
|
|
442
|
+
if (numClusters === 1 && totalPoints > 1) {
|
|
443
|
+
hints.push(`All ${String(totalPoints)} points formed a single cluster. Consider decreasing eps (currently ${String(eps)}m) to create more distinct clusters.`);
|
|
444
|
+
}
|
|
445
|
+
if (noisePoints > 0 && noisePoints > totalPoints * 0.5) {
|
|
446
|
+
hints.push(`${String(noisePoints)} of ${String(totalPoints)} points (${String(Math.round((noisePoints / totalPoints) * 100))}%) are noise. Consider increasing eps or decreasing minPoints (currently ${String(minPoints)}).`);
|
|
447
|
+
}
|
|
448
|
+
if (numClusters === 0 && totalPoints > 0) {
|
|
449
|
+
hints.push(`No clusters formed - all points are noise. Try increasing eps (currently ${String(eps)}m) or decreasing minPoints (currently ${String(minPoints)}).`);
|
|
450
|
+
}
|
|
451
|
+
response["notes"] =
|
|
452
|
+
"Noise points (cluster_id = NULL) are points not belonging to any cluster";
|
|
453
|
+
if (hints.length > 0) {
|
|
454
|
+
response["hints"] = hints;
|
|
455
|
+
}
|
|
456
|
+
response["parameterGuide"] = {
|
|
457
|
+
eps: `Distance threshold in meters. Larger values group more distant points together.`,
|
|
458
|
+
minPoints: `Minimum points required to form a cluster. Higher values create fewer, denser clusters.`,
|
|
459
|
+
};
|
|
396
460
|
}
|
|
397
|
-
|
|
398
|
-
|
|
461
|
+
else {
|
|
462
|
+
response["notes"] =
|
|
463
|
+
"K-Means will always assign all points to a cluster";
|
|
399
464
|
}
|
|
400
|
-
response
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
465
|
+
return response;
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof ZodError) {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: error.issues.map((i) => i.message).join("; "),
|
|
472
|
+
};
|
|
404
473
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
474
|
+
return {
|
|
475
|
+
success: false,
|
|
476
|
+
error: formatPostgresError(error, {
|
|
477
|
+
tool: "pg_geo_cluster",
|
|
478
|
+
table: params?.["table"] ??
|
|
479
|
+
undefined,
|
|
480
|
+
}),
|
|
408
481
|
};
|
|
409
482
|
}
|
|
410
|
-
else {
|
|
411
|
-
response["notes"] =
|
|
412
|
-
"K-Means will always assign all points to a cluster";
|
|
413
|
-
}
|
|
414
|
-
return response;
|
|
415
483
|
},
|
|
416
484
|
};
|
|
417
485
|
}
|