@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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { z } 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 { sanitizeWhereClause } from "../../../../utils/where-clause.js";
|
|
9
11
|
import { sanitizeIdentifier, sanitizeTableName, } from "../../../../utils/identifiers.js";
|
|
10
12
|
import { JsonbValidatePathOutputSchema, JsonbMergeOutputSchema, JsonbNormalizeOutputSchema, JsonbDiffOutputSchema, JsonbIndexSuggestOutputSchema, JsonbSecurityScanOutputSchema, JsonbStatsOutputSchema,
|
|
11
13
|
// Base schemas for MCP visibility (Split Schema pattern)
|
|
@@ -180,23 +182,30 @@ export function createJsonbMergeTool(adapter) {
|
|
|
180
182
|
annotations: readOnly("JSONB Merge"),
|
|
181
183
|
icons: getToolIcons("jsonb", readOnly("JSONB Merge")),
|
|
182
184
|
handler: async (params, _context) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
try {
|
|
186
|
+
const parsed = parseMergeParams(params);
|
|
187
|
+
const useDeep = parsed.deep !== false;
|
|
188
|
+
const useMergeArrays = parsed.mergeArrays === true;
|
|
189
|
+
if (useDeep) {
|
|
190
|
+
const merged = deepMergeObjects(parsed.base, parsed.overlay, useMergeArrays);
|
|
191
|
+
return { merged, deep: true, mergeArrays: useMergeArrays };
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const sql = `SELECT $1::jsonb || $2::jsonb as result`;
|
|
195
|
+
const result = await adapter.executeQuery(sql, [
|
|
196
|
+
toJsonString(parsed.base),
|
|
197
|
+
toJsonString(parsed.overlay),
|
|
198
|
+
]);
|
|
199
|
+
return { merged: result.rows?.[0]?.["result"], deep: false };
|
|
200
|
+
}
|
|
191
201
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return { merged: result.rows?.[0]?.["result"], deep: false };
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
error: formatPostgresError(error, {
|
|
206
|
+
tool: "pg_jsonb_merge",
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
200
209
|
}
|
|
201
210
|
},
|
|
202
211
|
};
|
|
@@ -214,74 +223,86 @@ export function createJsonbNormalizeTool(adapter) {
|
|
|
214
223
|
annotations: readOnly("JSONB Normalize"),
|
|
215
224
|
icons: getToolIcons("jsonb", readOnly("JSONB Normalize")),
|
|
216
225
|
handler: async (params, _context) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
]);
|
|
247
|
-
if (checkResult.rows && checkResult.rows.length > 0) {
|
|
248
|
-
rowIdExpr = '"id"';
|
|
226
|
+
try {
|
|
227
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
228
|
+
const parsed = JsonbNormalizeSchema.parse(params);
|
|
229
|
+
const table = parsed.table;
|
|
230
|
+
const column = parsed.column;
|
|
231
|
+
if (!table || !column) {
|
|
232
|
+
return { success: false, error: "table and column are required" };
|
|
233
|
+
}
|
|
234
|
+
const whereClause = parsed.where
|
|
235
|
+
? ` WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
236
|
+
: "";
|
|
237
|
+
const mode = parsed.mode ?? "keys";
|
|
238
|
+
// Validate mode parameter
|
|
239
|
+
const validModes = ["keys", "array", "pairs", "flatten"];
|
|
240
|
+
if (!validModes.includes(mode)) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: `pg_jsonb_normalize: Invalid mode '${mode}'. Valid modes: ${validModes.join(", ")}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// Validate schema existence for non-public schemas
|
|
247
|
+
const schemaName = parsed.schema ?? "public";
|
|
248
|
+
if (schemaName !== "public") {
|
|
249
|
+
const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
|
|
250
|
+
if (!schemaResult.rows || schemaResult.rows.length === 0) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
|
|
254
|
+
};
|
|
249
255
|
}
|
|
250
|
-
|
|
256
|
+
}
|
|
257
|
+
const tableName = sanitizeTableName(table, schemaName);
|
|
258
|
+
const columnName = sanitizeIdentifier(column);
|
|
259
|
+
// Determine row identifier column
|
|
260
|
+
let rowIdExpr;
|
|
261
|
+
let rowIdAlias = "source_id";
|
|
262
|
+
if (parsed.idColumn) {
|
|
263
|
+
rowIdExpr = sanitizeIdentifier(parsed.idColumn);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
try {
|
|
267
|
+
const checkSql = `SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = 'id' LIMIT 1`;
|
|
268
|
+
const checkResult = await adapter.executeQuery(checkSql, [
|
|
269
|
+
parsed.table,
|
|
270
|
+
]);
|
|
271
|
+
if (checkResult.rows && checkResult.rows.length > 0) {
|
|
272
|
+
rowIdExpr = '"id"';
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
rowIdExpr = "ctid::text";
|
|
276
|
+
rowIdAlias = "source_ctid";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
251
280
|
rowIdExpr = "ctid::text";
|
|
252
281
|
rowIdAlias = "source_ctid";
|
|
253
282
|
}
|
|
254
283
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
let sql;
|
|
285
|
+
if (mode === "array") {
|
|
286
|
+
sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, jsonb_array_elements(${columnName}) as element FROM ${tableName}${whereClause}`;
|
|
258
287
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, jsonb_array_elements(${columnName}) as element FROM ${tableName}${whereClause}`;
|
|
263
|
-
}
|
|
264
|
-
else if (mode === "flatten") {
|
|
265
|
-
// Recursive CTE to flatten nested objects to dot-notation keys
|
|
266
|
-
sql = `
|
|
267
|
-
WITH RECURSIVE
|
|
288
|
+
else if (mode === "flatten") {
|
|
289
|
+
sql = `
|
|
290
|
+
WITH RECURSIVE
|
|
268
291
|
source_rows AS (
|
|
269
292
|
SELECT ${rowIdExpr} as ${rowIdAlias}, ${columnName} as doc
|
|
270
293
|
FROM ${tableName}${whereClause}
|
|
271
294
|
),
|
|
272
295
|
flattened AS (
|
|
273
|
-
|
|
274
|
-
SELECT
|
|
296
|
+
SELECT
|
|
275
297
|
sr.${rowIdAlias},
|
|
276
298
|
kv.key as path,
|
|
277
299
|
kv.value,
|
|
278
300
|
jsonb_typeof(kv.value) as value_type
|
|
279
301
|
FROM source_rows sr, jsonb_each(sr.doc) kv
|
|
280
|
-
|
|
302
|
+
|
|
281
303
|
UNION ALL
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
SELECT
|
|
304
|
+
|
|
305
|
+
SELECT
|
|
285
306
|
f.${rowIdAlias},
|
|
286
307
|
f.path || '.' || kv.key,
|
|
287
308
|
kv.value,
|
|
@@ -289,27 +310,27 @@ export function createJsonbNormalizeTool(adapter) {
|
|
|
289
310
|
FROM flattened f, jsonb_each(f.value) kv
|
|
290
311
|
WHERE jsonb_typeof(f.value) = 'object'
|
|
291
312
|
)
|
|
292
|
-
SELECT ${rowIdAlias}, path as key, value, value_type FROM flattened
|
|
313
|
+
SELECT ${rowIdAlias}, path as key, value, value_type FROM flattened
|
|
293
314
|
WHERE value_type != 'object' OR value = '{}'::jsonb
|
|
294
315
|
ORDER BY ${rowIdAlias}, path
|
|
295
316
|
`;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
317
|
+
}
|
|
318
|
+
else if (mode === "pairs") {
|
|
319
|
+
sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each(${columnName}) ${whereClause}`;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
sql = `SELECT ${rowIdExpr} as ${rowIdAlias}, key, value FROM ${tableName}, jsonb_each_text(${columnName}) ${whereClause}`;
|
|
323
|
+
}
|
|
305
324
|
const result = await adapter.executeQuery(sql);
|
|
306
325
|
// Check for empty flatten results on array columns
|
|
307
326
|
if (mode === "flatten" && (result.rows?.length ?? 0) === 0) {
|
|
308
|
-
// Verify if this is because column contains arrays
|
|
309
327
|
const typeCheckSql = `SELECT jsonb_typeof(${columnName}) as type FROM ${tableName}${whereClause} LIMIT 1`;
|
|
310
328
|
const typeResult = await adapter.executeQuery(typeCheckSql);
|
|
311
329
|
if (typeResult.rows?.[0]?.["type"] === "array") {
|
|
312
|
-
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: `pg_jsonb_normalize flatten mode requires object columns. Column appears to contain arrays - use 'array' mode instead.`,
|
|
333
|
+
};
|
|
313
334
|
}
|
|
314
335
|
}
|
|
315
336
|
return { rows: result.rows, count: result.rows?.length ?? 0, mode };
|
|
@@ -318,14 +339,24 @@ export function createJsonbNormalizeTool(adapter) {
|
|
|
318
339
|
// Improve error for array columns with object-only modes
|
|
319
340
|
if (error instanceof Error &&
|
|
320
341
|
error.message.includes("cannot call jsonb_each")) {
|
|
321
|
-
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
error: `pg_jsonb_normalize requires object columns for this mode. For array columns, use mode: 'array'.`,
|
|
345
|
+
};
|
|
322
346
|
}
|
|
323
|
-
// Improve error for object columns with array mode
|
|
324
347
|
if (error instanceof Error &&
|
|
325
348
|
error.message.includes("cannot extract elements from an object")) {
|
|
326
|
-
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: `pg_jsonb_normalize 'array' mode requires array columns. For object columns, use mode: 'keys' or 'pairs'.`,
|
|
352
|
+
};
|
|
327
353
|
}
|
|
328
|
-
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
error: formatPostgresError(error, {
|
|
357
|
+
tool: "pg_jsonb_normalize",
|
|
358
|
+
}),
|
|
359
|
+
};
|
|
329
360
|
}
|
|
330
361
|
},
|
|
331
362
|
};
|
|
@@ -353,23 +384,26 @@ export function createJsonbDiffTool(adapter) {
|
|
|
353
384
|
annotations: readOnly("JSONB Diff"),
|
|
354
385
|
icons: getToolIcons("jsonb", readOnly("JSONB Diff")),
|
|
355
386
|
handler: async (params, _context) => {
|
|
356
|
-
let parsed;
|
|
357
387
|
try {
|
|
358
|
-
parsed
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
let parsed;
|
|
389
|
+
try {
|
|
390
|
+
parsed = JsonbDiffSchema.parse(params);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return {
|
|
394
|
+
success: false,
|
|
395
|
+
error: "pg_jsonb_diff requires two JSONB objects. Arrays and primitive values are not supported. Use {} format for both doc1 and doc2.",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
const sql = `
|
|
399
|
+
WITH
|
|
366
400
|
j1 AS (SELECT key, value FROM jsonb_each($1::jsonb)),
|
|
367
401
|
j2 AS (SELECT key, value FROM jsonb_each($2::jsonb))
|
|
368
|
-
SELECT
|
|
402
|
+
SELECT
|
|
369
403
|
COALESCE(j1.key, j2.key) as key,
|
|
370
404
|
j1.value as value1,
|
|
371
405
|
j2.value as value2,
|
|
372
|
-
CASE
|
|
406
|
+
CASE
|
|
373
407
|
WHEN j1.key IS NULL THEN 'added'
|
|
374
408
|
WHEN j2.key IS NULL THEN 'removed'
|
|
375
409
|
WHEN j1.value = j2.value THEN 'unchanged'
|
|
@@ -378,16 +412,25 @@ export function createJsonbDiffTool(adapter) {
|
|
|
378
412
|
FROM j1 FULL OUTER JOIN j2 ON j1.key = j2.key
|
|
379
413
|
WHERE j1.value IS DISTINCT FROM j2.value
|
|
380
414
|
`;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
415
|
+
const result = await adapter.executeQuery(sql, [
|
|
416
|
+
toJsonString(parsed.doc1),
|
|
417
|
+
toJsonString(parsed.doc2),
|
|
418
|
+
]);
|
|
419
|
+
return {
|
|
420
|
+
differences: result.rows,
|
|
421
|
+
hasDifferences: (result.rows?.length ?? 0) > 0,
|
|
422
|
+
comparison: "shallow",
|
|
423
|
+
hint: "Compares top-level keys only. Nested object changes show as modified.",
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
error: formatPostgresError(error, {
|
|
430
|
+
tool: "pg_jsonb_diff",
|
|
431
|
+
}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
391
434
|
},
|
|
392
435
|
};
|
|
393
436
|
}
|
|
@@ -404,86 +447,102 @@ export function createJsonbIndexSuggestTool(adapter) {
|
|
|
404
447
|
annotations: readOnly("JSONB Index Suggest"),
|
|
405
448
|
icons: getToolIcons("jsonb", readOnly("JSONB Index Suggest")),
|
|
406
449
|
handler: async (params, _context) => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
450
|
+
try {
|
|
451
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
452
|
+
const parsed = JsonbIndexSuggestSchema.parse(params);
|
|
453
|
+
const table = parsed.table;
|
|
454
|
+
const column = parsed.column;
|
|
455
|
+
if (!table || !column) {
|
|
456
|
+
return { success: false, error: "table and column are required" };
|
|
457
|
+
}
|
|
458
|
+
const sample = parsed.sampleSize ?? 1000;
|
|
459
|
+
const whereClause = parsed.where
|
|
460
|
+
? ` WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
461
|
+
: "";
|
|
462
|
+
// Validate schema existence for non-public schemas
|
|
463
|
+
const schemaName = parsed.schema ?? "public";
|
|
464
|
+
if (schemaName !== "public") {
|
|
465
|
+
const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
|
|
466
|
+
if (!schemaResult.rows || schemaResult.rows.length === 0) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const tableName = sanitizeTableName(table, schemaName);
|
|
474
|
+
const columnName = sanitizeIdentifier(column);
|
|
475
|
+
const keySql = `
|
|
476
|
+
SELECT key, COUNT(*) as frequency,
|
|
420
477
|
jsonb_typeof(value) as value_type
|
|
421
478
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
|
|
422
|
-
jsonb_each(${columnName})
|
|
479
|
+
jsonb_each(${columnName})
|
|
423
480
|
GROUP BY key, jsonb_typeof(value)
|
|
424
481
|
ORDER BY frequency DESC
|
|
425
482
|
LIMIT 20
|
|
426
483
|
`;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
keyResult = await adapter.executeQuery(keySql);
|
|
430
|
-
}
|
|
431
|
-
catch (error) {
|
|
432
|
-
if (error instanceof Error &&
|
|
433
|
-
(error.message.includes("function jsonb_each") ||
|
|
434
|
-
error.message.includes("cannot call jsonb_each"))) {
|
|
435
|
-
throw new Error(`pg_jsonb_index_suggest requires JSONB objects (not arrays). Column '${column}' may not be JSONB type or contains arrays.`);
|
|
436
|
-
}
|
|
437
|
-
throw error;
|
|
438
|
-
}
|
|
439
|
-
const indexSql = `
|
|
484
|
+
const keyResult = await adapter.executeQuery(keySql);
|
|
485
|
+
const indexSql = `
|
|
440
486
|
SELECT indexname, indexdef
|
|
441
487
|
FROM pg_indexes
|
|
442
488
|
WHERE tablename = $1
|
|
443
489
|
AND indexdef LIKE '%' || $2 || '%'
|
|
444
490
|
`;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if ((indexResult.rows?.length ?? 0) === 0 && keys.length > 0) {
|
|
458
|
-
recommendations.push(`CREATE INDEX ON ${tableName} USING GIN (${columnName})`);
|
|
459
|
-
}
|
|
460
|
-
for (const keyInfo of keys.slice(0, 5)) {
|
|
461
|
-
if (keyInfo.frequency > sample * 0.5) {
|
|
462
|
-
recommendations.push(`CREATE INDEX ON ${tableName} ((${columnName} ->> '${keyInfo.key.replace(/'/g, "''")}'))`);
|
|
491
|
+
const indexResult = await adapter.executeQuery(indexSql, [
|
|
492
|
+
parsed.table,
|
|
493
|
+
parsed.column,
|
|
494
|
+
]);
|
|
495
|
+
const recommendations = [];
|
|
496
|
+
const keys = (keyResult.rows ?? []).map((row) => ({
|
|
497
|
+
key: row["key"],
|
|
498
|
+
frequency: Number(row["frequency"]),
|
|
499
|
+
value_type: row["value_type"],
|
|
500
|
+
}));
|
|
501
|
+
if ((indexResult.rows?.length ?? 0) === 0 && keys.length > 0) {
|
|
502
|
+
recommendations.push(`CREATE INDEX ON ${tableName} USING GIN (${columnName})`);
|
|
463
503
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
existingIndexes: indexResult.rows,
|
|
469
|
-
recommendations,
|
|
470
|
-
};
|
|
471
|
-
// Add explanation when no recommendations
|
|
472
|
-
if (recommendations.length === 0) {
|
|
473
|
-
if ((indexResult.rows?.length ?? 0) > 0) {
|
|
474
|
-
response.hint =
|
|
475
|
-
"No new recommendations - existing indexes already cover this column";
|
|
476
|
-
}
|
|
477
|
-
else if (keys.length === 0) {
|
|
478
|
-
response.hint =
|
|
479
|
-
"No recommendations - table is empty or column has no keys to analyze";
|
|
504
|
+
for (const keyInfo of keys.slice(0, 5)) {
|
|
505
|
+
if (keyInfo.frequency > sample * 0.5) {
|
|
506
|
+
recommendations.push(`CREATE INDEX ON ${tableName} ((${columnName} ->> '${keyInfo.key.replace(/'/g, "''")}'))`);
|
|
507
|
+
}
|
|
480
508
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
509
|
+
const response = {
|
|
510
|
+
keyDistribution: keys,
|
|
511
|
+
existingIndexes: indexResult.rows,
|
|
512
|
+
recommendations,
|
|
513
|
+
};
|
|
514
|
+
if (recommendations.length === 0) {
|
|
515
|
+
if ((indexResult.rows?.length ?? 0) > 0) {
|
|
516
|
+
response.hint =
|
|
517
|
+
"No new recommendations - existing indexes already cover this column";
|
|
518
|
+
}
|
|
519
|
+
else if (keys.length === 0) {
|
|
520
|
+
response.hint =
|
|
521
|
+
"No recommendations - table is empty or column has no keys to analyze";
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
response.hint =
|
|
525
|
+
"No recommendations - no keys appeared in >50% of sampled rows";
|
|
526
|
+
}
|
|
484
527
|
}
|
|
528
|
+
return response;
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (error instanceof Error &&
|
|
532
|
+
(error.message.includes("function jsonb_each") ||
|
|
533
|
+
error.message.includes("cannot call jsonb_each"))) {
|
|
534
|
+
return {
|
|
535
|
+
success: false,
|
|
536
|
+
error: `pg_jsonb_index_suggest requires JSONB objects (not arrays). Column may not be JSONB type or contains arrays.`,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
success: false,
|
|
541
|
+
error: formatPostgresError(error, {
|
|
542
|
+
tool: "pg_jsonb_index_suggest",
|
|
543
|
+
}),
|
|
544
|
+
};
|
|
485
545
|
}
|
|
486
|
-
return response;
|
|
487
546
|
},
|
|
488
547
|
};
|
|
489
548
|
}
|
|
@@ -500,85 +559,105 @@ export function createJsonbSecurityScanTool(adapter) {
|
|
|
500
559
|
annotations: readOnly("JSONB Security Scan"),
|
|
501
560
|
icons: getToolIcons("jsonb", readOnly("JSONB Security Scan")),
|
|
502
561
|
handler: async (params, _context) => {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
562
|
+
try {
|
|
563
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
564
|
+
const parsed = JsonbSecurityScanSchema.parse(params);
|
|
565
|
+
const table = parsed.table;
|
|
566
|
+
const column = parsed.column;
|
|
567
|
+
if (!table || !column) {
|
|
568
|
+
return { success: false, error: "table and column are required" };
|
|
569
|
+
}
|
|
570
|
+
const sample = parsed.sampleSize ?? 100;
|
|
571
|
+
const whereClause = parsed.where
|
|
572
|
+
? ` WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
573
|
+
: "";
|
|
574
|
+
const issues = [];
|
|
575
|
+
// Validate schema existence for non-public schemas
|
|
576
|
+
const schemaName = parsed.schema ?? "public";
|
|
577
|
+
if (schemaName !== "public") {
|
|
578
|
+
const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
|
|
579
|
+
if (!schemaResult.rows || schemaResult.rows.length === 0) {
|
|
580
|
+
return {
|
|
581
|
+
success: false,
|
|
582
|
+
error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const tableName = sanitizeTableName(table, schemaName);
|
|
587
|
+
const columnName = sanitizeIdentifier(column);
|
|
588
|
+
// Count actual rows scanned
|
|
589
|
+
const countSql = `SELECT COUNT(*) as count FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t`;
|
|
590
|
+
const countResult = await adapter.executeQuery(countSql);
|
|
591
|
+
const actualRowsScanned = Number(countResult.rows?.[0]?.["count"] ?? 0);
|
|
592
|
+
const sensitiveKeysSql = `
|
|
520
593
|
SELECT key, COUNT(*) as count
|
|
521
594
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
|
|
522
595
|
jsonb_each_text(${columnName})
|
|
523
|
-
WHERE lower(key) IN ('password', 'secret', 'token', 'api_key', 'apikey',
|
|
596
|
+
WHERE lower(key) IN ('password', 'secret', 'token', 'api_key', 'apikey',
|
|
524
597
|
'auth', 'credential', 'ssn', 'credit_card', 'cvv')
|
|
525
598
|
GROUP BY key
|
|
526
599
|
`;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
error.message.includes("cannot call jsonb_each"))) {
|
|
535
|
-
throw new Error(`pg_jsonb_security_scan requires JSONB objects. Column '${column}' may contain arrays or non-JSONB data.`);
|
|
600
|
+
const sensitiveResult = await adapter.executeQuery(sensitiveKeysSql);
|
|
601
|
+
for (const row of (sensitiveResult.rows ?? [])) {
|
|
602
|
+
issues.push({
|
|
603
|
+
type: "sensitive_key",
|
|
604
|
+
key: row.key,
|
|
605
|
+
count: Number(row.count),
|
|
606
|
+
});
|
|
536
607
|
}
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
for (const row of (sensitiveResult.rows ?? [])) {
|
|
540
|
-
issues.push({
|
|
541
|
-
type: "sensitive_key",
|
|
542
|
-
key: row.key,
|
|
543
|
-
count: Number(row.count),
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
const injectionSql = `
|
|
608
|
+
const injectionSql = `
|
|
547
609
|
SELECT key, COUNT(*) as count
|
|
548
610
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
|
|
549
611
|
jsonb_each_text(${columnName})
|
|
550
612
|
WHERE value ~* '(\\bSELECT\\s+.+\\bFROM\\b|\\bINSERT\\s+INTO\\b|\\bUPDATE\\s+.+\\bSET\\b|\\bDELETE\\s+FROM\\b|\\bDROP\\s+(TABLE|DATABASE|INDEX)\\b|\\bUNION\\s+(ALL\\s+)?SELECT\\b|--\\s*$|;\\s*(SELECT|INSERT|UPDATE|DELETE))'
|
|
551
613
|
GROUP BY key
|
|
552
614
|
`;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
615
|
+
const injectionResult = await adapter.executeQuery(injectionSql);
|
|
616
|
+
for (const row of (injectionResult.rows ?? [])) {
|
|
617
|
+
issues.push({
|
|
618
|
+
type: "sql_injection_pattern",
|
|
619
|
+
key: row.key,
|
|
620
|
+
count: Number(row.count),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
// XSS pattern detection
|
|
624
|
+
const xssSql = `
|
|
563
625
|
SELECT key, COUNT(*) as count
|
|
564
626
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
|
|
565
627
|
jsonb_each_text(${columnName})
|
|
566
628
|
WHERE value ~* '(<script|javascript:|on(click|load|error|mouseover)\\s*=|<iframe|<object|<embed|<svg[^>]+on|<img[^>]+onerror)'
|
|
567
629
|
GROUP BY key
|
|
568
630
|
`;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
631
|
+
const xssResult = await adapter.executeQuery(xssSql);
|
|
632
|
+
for (const row of (xssResult.rows ?? [])) {
|
|
633
|
+
issues.push({
|
|
634
|
+
type: "xss_pattern",
|
|
635
|
+
key: row.key,
|
|
636
|
+
count: Number(row.count),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
scannedRows: actualRowsScanned,
|
|
641
|
+
issues,
|
|
642
|
+
riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error instanceof Error &&
|
|
647
|
+
(error.message.includes("function jsonb_each") ||
|
|
648
|
+
error.message.includes("cannot call jsonb_each"))) {
|
|
649
|
+
return {
|
|
650
|
+
success: false,
|
|
651
|
+
error: `pg_jsonb_security_scan requires JSONB objects. Column may contain arrays or non-JSONB data.`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
success: false,
|
|
656
|
+
error: formatPostgresError(error, {
|
|
657
|
+
tool: "pg_jsonb_security_scan",
|
|
658
|
+
}),
|
|
659
|
+
};
|
|
576
660
|
}
|
|
577
|
-
return {
|
|
578
|
-
scannedRows: actualRowsScanned,
|
|
579
|
-
issues,
|
|
580
|
-
riskLevel: issues.length === 0 ? "low" : issues.length < 3 ? "medium" : "high",
|
|
581
|
-
};
|
|
582
661
|
},
|
|
583
662
|
};
|
|
584
663
|
}
|
|
@@ -595,38 +674,51 @@ export function createJsonbStatsTool(adapter) {
|
|
|
595
674
|
annotations: readOnly("JSONB Stats"),
|
|
596
675
|
icons: getToolIcons("jsonb", readOnly("JSONB Stats")),
|
|
597
676
|
handler: async (params, _context) => {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
677
|
+
try {
|
|
678
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
679
|
+
const parsed = JsonbStatsSchema.parse(params);
|
|
680
|
+
const table = parsed.table;
|
|
681
|
+
const column = parsed.column;
|
|
682
|
+
if (!table || !column) {
|
|
683
|
+
return { success: false, error: "table and column are required" };
|
|
684
|
+
}
|
|
685
|
+
const sample = parsed.sampleSize ?? 1000;
|
|
686
|
+
const whereClause = parsed.where
|
|
687
|
+
? ` WHERE ${sanitizeWhereClause(parsed.where)}`
|
|
688
|
+
: "";
|
|
689
|
+
// Validate schema existence for non-public schemas
|
|
690
|
+
const schemaName = parsed.schema ?? "public";
|
|
691
|
+
if (schemaName !== "public") {
|
|
692
|
+
const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
|
|
693
|
+
if (!schemaResult.rows || schemaResult.rows.length === 0) {
|
|
694
|
+
return {
|
|
695
|
+
success: false,
|
|
696
|
+
error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const tableName = sanitizeTableName(table, schemaName);
|
|
701
|
+
const columnName = sanitizeIdentifier(column);
|
|
702
|
+
const basicSql = `
|
|
703
|
+
SELECT
|
|
611
704
|
COUNT(*) as total_rows,
|
|
612
705
|
COUNT(${columnName}) as non_null_count,
|
|
613
706
|
AVG(length(${columnName}::text))::int as avg_size_bytes,
|
|
614
707
|
MAX(length(${columnName}::text)) as max_size_bytes
|
|
615
708
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
|
|
616
709
|
`;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const keySql = `
|
|
710
|
+
const basicResult = await adapter.executeQuery(basicSql);
|
|
711
|
+
const basics = basicResult.rows?.[0];
|
|
712
|
+
const basicsNormalized = basics
|
|
713
|
+
? {
|
|
714
|
+
total_rows: Number(basics["total_rows"]),
|
|
715
|
+
non_null_count: Number(basics["non_null_count"]),
|
|
716
|
+
avg_size_bytes: Number(basics["avg_size_bytes"]),
|
|
717
|
+
max_size_bytes: Number(basics["max_size_bytes"]),
|
|
718
|
+
}
|
|
719
|
+
: undefined;
|
|
720
|
+
const keyLimit = parsed.topKeysLimit ?? 20;
|
|
721
|
+
const keySql = `
|
|
630
722
|
SELECT key, COUNT(*) as frequency
|
|
631
723
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t,
|
|
632
724
|
jsonb_object_keys(${columnName}) key
|
|
@@ -634,57 +726,62 @@ export function createJsonbStatsTool(adapter) {
|
|
|
634
726
|
ORDER BY frequency DESC
|
|
635
727
|
LIMIT ${String(keyLimit)}
|
|
636
728
|
`;
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}));
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
// Gracefully handle array columns (jsonb_object_keys fails on arrays)
|
|
648
|
-
if (error instanceof Error &&
|
|
649
|
-
error.message.includes("cannot call jsonb_object_keys")) {
|
|
650
|
-
// Leave topKeys empty for array columns - this is valid
|
|
729
|
+
let topKeys = [];
|
|
730
|
+
try {
|
|
731
|
+
const keyResult = await adapter.executeQuery(keySql);
|
|
732
|
+
topKeys = (keyResult.rows ?? []).map((row) => ({
|
|
733
|
+
key: row["key"],
|
|
734
|
+
frequency: Number(row["frequency"]),
|
|
735
|
+
}));
|
|
651
736
|
}
|
|
652
|
-
|
|
653
|
-
|
|
737
|
+
catch (error) {
|
|
738
|
+
// Gracefully handle array columns (jsonb_object_keys fails on arrays)
|
|
739
|
+
if (error instanceof Error &&
|
|
740
|
+
error.message.includes("cannot call jsonb_object_keys")) {
|
|
741
|
+
// Leave topKeys empty for array columns - this is valid
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
throw error; // Re-throw to be caught by outer catch
|
|
745
|
+
}
|
|
654
746
|
}
|
|
655
|
-
|
|
656
|
-
const typeSql = `
|
|
747
|
+
const typeSql = `
|
|
657
748
|
SELECT jsonb_typeof(${columnName}) as type, COUNT(*) as count
|
|
658
749
|
FROM (SELECT * FROM ${tableName}${whereClause} LIMIT ${String(sample)}) t
|
|
659
750
|
GROUP BY jsonb_typeof(${columnName})
|
|
660
751
|
`;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
752
|
+
const typeResult = await adapter.executeQuery(typeSql);
|
|
753
|
+
const typeDistribution = (typeResult.rows ?? []).map((row) => ({
|
|
754
|
+
type: row["type"],
|
|
755
|
+
count: Number(row["count"]),
|
|
756
|
+
}));
|
|
757
|
+
const sqlNullCount = typeDistribution.find((t) => t.type === null)?.count ?? 0;
|
|
758
|
+
const hasNullColumns = sqlNullCount > 0;
|
|
759
|
+
const isArrayColumn = typeDistribution.some((t) => t.type === "array");
|
|
760
|
+
let hint;
|
|
761
|
+
if (hasNullColumns) {
|
|
762
|
+
hint =
|
|
763
|
+
"typeDistribution null type represents SQL NULL columns, not JSON null values";
|
|
764
|
+
}
|
|
765
|
+
else if (topKeys.length === 0 && isArrayColumn) {
|
|
766
|
+
hint =
|
|
767
|
+
'topKeys empty for array columns - use pg_jsonb_normalize mode: "array" to analyze elements';
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
basics: basicsNormalized,
|
|
771
|
+
topKeys,
|
|
772
|
+
typeDistribution,
|
|
773
|
+
sqlNullCount,
|
|
774
|
+
hint,
|
|
775
|
+
};
|
|
676
776
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
777
|
+
catch (error) {
|
|
778
|
+
return {
|
|
779
|
+
success: false,
|
|
780
|
+
error: formatPostgresError(error, {
|
|
781
|
+
tool: "pg_jsonb_stats",
|
|
782
|
+
}),
|
|
783
|
+
};
|
|
680
784
|
}
|
|
681
|
-
return {
|
|
682
|
-
basics: basicsNormalized,
|
|
683
|
-
topKeys,
|
|
684
|
-
typeDistribution,
|
|
685
|
-
sqlNullCount,
|
|
686
|
-
hint,
|
|
687
|
-
};
|
|
688
785
|
},
|
|
689
786
|
};
|
|
690
787
|
}
|