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