@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
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Core JSONB operations including extract, set, insert, delete, contains, path query, aggregation, and type checks.
|
|
5
5
|
*/
|
|
6
|
-
import { z } from "zod";
|
|
6
|
+
import { z, ZodError } from "zod";
|
|
7
7
|
import { readOnly, write } from "../../../../utils/annotations.js";
|
|
8
8
|
import { getToolIcons } from "../../../../utils/icons.js";
|
|
9
|
+
import { formatPostgresError } from "../core/error-helpers.js";
|
|
10
|
+
import { sanitizeTableName, sanitizeIdentifier, } from "../../../../utils/identifiers.js";
|
|
9
11
|
import {
|
|
10
12
|
// Base schemas (for MCP inputSchema visibility)
|
|
11
13
|
JsonbExtractSchemaBase, JsonbSetSchemaBase, JsonbContainsSchemaBase, JsonbPathQuerySchemaBase, JsonbInsertSchemaBase, JsonbDeleteSchemaBase, JsonbTypeofSchemaBase, JsonbKeysSchemaBase, JsonbStripNullsSchemaBase, JsonbAggSchemaBase,
|
|
@@ -22,6 +24,28 @@ JsonbExtractOutputSchema, JsonbSetOutputSchema, JsonbInsertOutputSchema, JsonbDe
|
|
|
22
24
|
function toJsonString(value) {
|
|
23
25
|
return JSON.stringify(value);
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve table and schema for JSONB tools.
|
|
29
|
+
* Validates schema existence when non-public, returns schema-qualified table name.
|
|
30
|
+
* Returns [qualifiedTable, null] on success, or [null, errorResponse] on failure.
|
|
31
|
+
*/
|
|
32
|
+
async function resolveJsonbTable(adapter, table, schema) {
|
|
33
|
+
const schemaName = schema ?? "public";
|
|
34
|
+
// Validate schema existence for non-public schemas
|
|
35
|
+
if (schemaName !== "public") {
|
|
36
|
+
const schemaResult = await adapter.executeQuery(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schemaName]);
|
|
37
|
+
if (!schemaResult.rows || schemaResult.rows.length === 0) {
|
|
38
|
+
return [
|
|
39
|
+
null,
|
|
40
|
+
{
|
|
41
|
+
success: false,
|
|
42
|
+
error: `Schema '${schemaName}' does not exist. Use pg_list_objects with type 'table' to see available schemas.`,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [sanitizeTableName(table, schemaName), null];
|
|
48
|
+
}
|
|
25
49
|
export function createJsonbExtractTool(adapter) {
|
|
26
50
|
return {
|
|
27
51
|
name: "pg_jsonb_extract",
|
|
@@ -32,50 +56,70 @@ export function createJsonbExtractTool(adapter) {
|
|
|
32
56
|
annotations: readOnly("JSONB Extract"),
|
|
33
57
|
icons: getToolIcons("jsonb", readOnly("JSONB Extract")),
|
|
34
58
|
handler: async (params, _context) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JsonbExtractSchema.parse(params);
|
|
61
|
+
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
62
|
+
const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
|
|
63
|
+
// Use normalizePathToArray for PostgreSQL #> operator
|
|
64
|
+
const pathArray = normalizePathToArray(parsed.path);
|
|
65
|
+
// After preprocess and refine, table and column are guaranteed set
|
|
66
|
+
const table = parsed.table ?? parsed.tableName;
|
|
67
|
+
const column = parsed.column ?? parsed.col;
|
|
68
|
+
if (!table || !column) {
|
|
69
|
+
return { success: false, error: "table and column are required" };
|
|
70
|
+
}
|
|
71
|
+
// Validate schema and build qualified table name
|
|
72
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
73
|
+
if (tableError)
|
|
74
|
+
return tableError;
|
|
75
|
+
// Build select expression with optional additional columns
|
|
76
|
+
let selectExpr = `${sanitizeIdentifier(column)} #> $1 as extracted_value`;
|
|
77
|
+
if (parsed.select !== undefined && parsed.select.length > 0) {
|
|
78
|
+
const additionalCols = parsed.select
|
|
79
|
+
.map((c) => {
|
|
80
|
+
// Handle expressions vs simple column names
|
|
81
|
+
const needsQuote = !c.includes("->") &&
|
|
82
|
+
!c.includes("(") &&
|
|
83
|
+
!c.includes("::") &&
|
|
84
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(c);
|
|
85
|
+
return needsQuote ? `"${c}"` : c;
|
|
86
|
+
})
|
|
87
|
+
.join(", ");
|
|
88
|
+
selectExpr = `${additionalCols}, ${selectExpr}`;
|
|
89
|
+
}
|
|
90
|
+
const sql = `SELECT ${selectExpr} FROM ${qualifiedTable}${whereClause}${limitClause}`;
|
|
91
|
+
const result = await adapter.executeQuery(sql, [pathArray]);
|
|
92
|
+
// If select columns were provided, return full row objects
|
|
93
|
+
if (parsed.select !== undefined && parsed.select.length > 0) {
|
|
94
|
+
const rows = result.rows?.map((r) => {
|
|
95
|
+
// Rename extracted_value back to 'value' for consistency
|
|
96
|
+
const row = {};
|
|
97
|
+
for (const [key, val] of Object.entries(r)) {
|
|
98
|
+
if (key === "extracted_value") {
|
|
99
|
+
row["value"] = val;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
row[key] = val;
|
|
103
|
+
}
|
|
74
104
|
}
|
|
105
|
+
return row;
|
|
106
|
+
});
|
|
107
|
+
const allNulls = rows?.every((r) => r["value"] === null) ?? false;
|
|
108
|
+
const response = {
|
|
109
|
+
rows,
|
|
110
|
+
count: rows?.length ?? 0,
|
|
111
|
+
};
|
|
112
|
+
if (allNulls && (rows?.length ?? 0) > 0) {
|
|
113
|
+
response.hint =
|
|
114
|
+
"All values are null - path may not exist in data. Use pg_jsonb_typeof to check.";
|
|
75
115
|
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
116
|
+
return response;
|
|
117
|
+
}
|
|
118
|
+
// Original behavior: return just the extracted values
|
|
119
|
+
// Wrap each value in an object with 'value' key for consistency with select mode
|
|
120
|
+
const rows = result.rows?.map((r) => ({ value: r["extracted_value"] }));
|
|
121
|
+
// Check if all results are null (path may not exist)
|
|
122
|
+
const allNulls = rows?.every((r) => r.value === null) ?? false;
|
|
79
123
|
const response = {
|
|
80
124
|
rows,
|
|
81
125
|
count: rows?.length ?? 0,
|
|
@@ -86,20 +130,14 @@ export function createJsonbExtractTool(adapter) {
|
|
|
86
130
|
}
|
|
87
131
|
return response;
|
|
88
132
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
count: rows?.length ?? 0,
|
|
97
|
-
};
|
|
98
|
-
if (allNulls && (rows?.length ?? 0) > 0) {
|
|
99
|
-
response.hint =
|
|
100
|
-
"All values are null - path may not exist in data. Use pg_jsonb_typeof to check.";
|
|
133
|
+
catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: formatPostgresError(error, {
|
|
137
|
+
tool: "pg_jsonb_extract",
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
101
140
|
}
|
|
102
|
-
return response;
|
|
103
141
|
},
|
|
104
142
|
};
|
|
105
143
|
}
|
|
@@ -113,72 +151,92 @@ export function createJsonbSetTool(adapter) {
|
|
|
113
151
|
annotations: write("JSONB Set"),
|
|
114
152
|
icons: getToolIcons("jsonb", write("JSONB Set")),
|
|
115
153
|
handler: async (params, _context) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JsonbSetSchema.parse(params);
|
|
156
|
+
// Resolve table/column from optional aliases
|
|
157
|
+
const table = parsed.table ?? parsed.tableName;
|
|
158
|
+
const column = parsed.column ?? parsed.col;
|
|
159
|
+
if (!table || !column) {
|
|
160
|
+
return { success: false, error: "table and column are required" };
|
|
161
|
+
}
|
|
162
|
+
const { value, where, createMissing } = parsed;
|
|
163
|
+
// Validate schema and build qualified table name
|
|
164
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
165
|
+
if (tableError)
|
|
166
|
+
return tableError;
|
|
167
|
+
// Normalize path to array format
|
|
168
|
+
const path = normalizePathToArray(parsed.path);
|
|
169
|
+
// Validate required 'where' parameter
|
|
170
|
+
if (!where || where.trim() === "") {
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error: 'pg_jsonb_set requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// Validate value is provided (undefined would set column to null)
|
|
177
|
+
if (value === undefined) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
error: "pg_jsonb_set requires a value parameter. To remove a key, use pg_jsonb_delete instead.",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const createFlag = createMissing !== false;
|
|
184
|
+
// Handle empty path - replace entire column value
|
|
185
|
+
if (path.length === 0) {
|
|
186
|
+
const sql = `UPDATE ${qualifiedTable} SET "${column}" = $1::jsonb WHERE ${where}`;
|
|
187
|
+
const result = await adapter.executeQuery(sql, [toJsonString(value)]);
|
|
188
|
+
return {
|
|
189
|
+
rowsAffected: result.rowsAffected,
|
|
190
|
+
hint: "Replaced entire column value (empty path)",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// For deep nested paths with createMissing=true, build intermediate objects
|
|
194
|
+
// PostgreSQL's jsonb_set only creates one level, so we nest calls for deep paths
|
|
195
|
+
let sql;
|
|
196
|
+
if (createFlag && path.length > 1) {
|
|
197
|
+
// Build nested jsonb_set calls to ensure each intermediate path exists
|
|
198
|
+
// Start with COALESCE to handle NULL columns
|
|
199
|
+
let expr = `COALESCE("${column}", '{}'::jsonb)`;
|
|
200
|
+
// For each intermediate level, wrap in jsonb_set to initialize to {}
|
|
201
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
202
|
+
const subPath = path.slice(0, i + 1);
|
|
203
|
+
const pathStr = "{" + subPath.join(",") + "}";
|
|
204
|
+
// Use COALESCE on the extraction from current expr, not original column
|
|
205
|
+
// This properly chains the nested creation
|
|
206
|
+
expr = `jsonb_set(${expr}, '${pathStr}'::text[], COALESCE((${expr}) #> '${pathStr}'::text[], '{}'::jsonb), true)`;
|
|
207
|
+
}
|
|
208
|
+
// Final set with actual value
|
|
209
|
+
const fullPathStr = "{" + path.join(",") + "}";
|
|
210
|
+
expr = `jsonb_set(${expr}, '${fullPathStr}'::text[], $1::jsonb, true)`;
|
|
211
|
+
sql = `UPDATE ${qualifiedTable} SET "${column}" = ${expr} WHERE ${where}`;
|
|
212
|
+
const result = await adapter.executeQuery(sql, [toJsonString(value)]);
|
|
213
|
+
return {
|
|
214
|
+
rowsAffected: result.rowsAffected,
|
|
215
|
+
hint: "rowsAffected counts matched rows, not path creations",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Use COALESCE to handle NULL columns - initialize to empty object
|
|
220
|
+
sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_set(COALESCE("${column}", '{}'::jsonb), $1, $2::jsonb, $3) WHERE ${where}`;
|
|
221
|
+
const result = await adapter.executeQuery(sql, [
|
|
222
|
+
path,
|
|
223
|
+
toJsonString(value),
|
|
224
|
+
createFlag,
|
|
225
|
+
]);
|
|
226
|
+
const hint = createFlag
|
|
227
|
+
? "NULL columns initialized to {}; createMissing creates path if absent"
|
|
228
|
+
: "createMissing=false: path must exist or value won't be set";
|
|
229
|
+
return { rowsAffected: result.rowsAffected, hint };
|
|
230
|
+
}
|
|
143
231
|
}
|
|
144
|
-
|
|
145
|
-
// PostgreSQL's jsonb_set only creates one level, so we nest calls for deep paths
|
|
146
|
-
let sql;
|
|
147
|
-
if (createFlag && path.length > 1) {
|
|
148
|
-
// Build nested jsonb_set calls to ensure each intermediate path exists
|
|
149
|
-
// Start with COALESCE to handle NULL columns
|
|
150
|
-
let expr = `COALESCE("${column}", '{}'::jsonb)`;
|
|
151
|
-
// For each intermediate level, wrap in jsonb_set to initialize to {}
|
|
152
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
153
|
-
const subPath = path.slice(0, i + 1);
|
|
154
|
-
const pathStr = "{" + subPath.join(",") + "}";
|
|
155
|
-
// Use COALESCE on the extraction from current expr, not original column
|
|
156
|
-
// This properly chains the nested creation
|
|
157
|
-
expr = `jsonb_set(${expr}, '${pathStr}'::text[], COALESCE((${expr}) #> '${pathStr}'::text[], '{}'::jsonb), true)`;
|
|
158
|
-
}
|
|
159
|
-
// Final set with actual value
|
|
160
|
-
const fullPathStr = "{" + path.join(",") + "}";
|
|
161
|
-
expr = `jsonb_set(${expr}, '${fullPathStr}'::text[], $1::jsonb, true)`;
|
|
162
|
-
sql = `UPDATE "${table}" SET "${column}" = ${expr} WHERE ${where}`;
|
|
163
|
-
const result = await adapter.executeQuery(sql, [toJsonString(value)]);
|
|
232
|
+
catch (error) {
|
|
164
233
|
return {
|
|
165
|
-
|
|
166
|
-
|
|
234
|
+
success: false,
|
|
235
|
+
error: formatPostgresError(error, {
|
|
236
|
+
tool: "pg_jsonb_set",
|
|
237
|
+
}),
|
|
167
238
|
};
|
|
168
239
|
}
|
|
169
|
-
else {
|
|
170
|
-
// Use COALESCE to handle NULL columns - initialize to empty object
|
|
171
|
-
sql = `UPDATE "${table}" SET "${column}" = jsonb_set(COALESCE("${column}", '{}'::jsonb), $1, $2::jsonb, $3) WHERE ${where}`;
|
|
172
|
-
const result = await adapter.executeQuery(sql, [
|
|
173
|
-
path,
|
|
174
|
-
toJsonString(value),
|
|
175
|
-
createFlag,
|
|
176
|
-
]);
|
|
177
|
-
const hint = createFlag
|
|
178
|
-
? "NULL columns initialized to {}; createMissing creates path if absent"
|
|
179
|
-
: "createMissing=false: path must exist or value won't be set";
|
|
180
|
-
return { rowsAffected: result.rowsAffected, hint };
|
|
181
|
-
}
|
|
182
240
|
},
|
|
183
241
|
};
|
|
184
242
|
}
|
|
@@ -192,52 +250,68 @@ export function createJsonbInsertTool(adapter) {
|
|
|
192
250
|
annotations: write("JSONB Insert"),
|
|
193
251
|
icons: getToolIcons("jsonb", write("JSONB Insert")),
|
|
194
252
|
handler: async (params, _context) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Normalize path - convert numeric segments to numbers for PostgreSQL
|
|
203
|
-
const path = normalizePathForInsert(parsed.path);
|
|
204
|
-
// Validate required 'where' parameter
|
|
205
|
-
if (!parsed.where || parsed.where.trim() === "") {
|
|
206
|
-
throw new Error('pg_jsonb_insert requires a WHERE clause to identify rows to update. Example: where: "id = 1"');
|
|
207
|
-
}
|
|
208
|
-
// Check for NULL columns first - jsonb_insert requires existing array context
|
|
209
|
-
const checkSql = `SELECT COUNT(*) as null_count FROM "${table}" WHERE ${parsed.where} AND "${column}" IS NULL`;
|
|
210
|
-
const checkResult = await adapter.executeQuery(checkSql);
|
|
211
|
-
const nullCount = Number(checkResult.rows?.[0]?.["null_count"] ?? 0);
|
|
212
|
-
if (nullCount > 0) {
|
|
213
|
-
throw new Error(`pg_jsonb_insert cannot operate on NULL columns. Use pg_jsonb_set to initialize the column first: pg_jsonb_set({table: "${table}", column: "${column}", path: "myarray", value: [], where: "..."})`);
|
|
214
|
-
}
|
|
215
|
-
// Validate target path points to an array, not an object
|
|
216
|
-
// Get the parent path (one level up from where we're inserting)
|
|
217
|
-
const parentPath = path.slice(0, -1);
|
|
218
|
-
if (parentPath.length === 0) {
|
|
219
|
-
// Inserting at root level - check column type
|
|
220
|
-
const typeCheckSql = `SELECT jsonb_typeof("${column}") as type FROM "${table}" WHERE ${parsed.where} LIMIT 1`;
|
|
221
|
-
const typeResult = await adapter.executeQuery(typeCheckSql);
|
|
222
|
-
const columnType = typeResult.rows?.[0]?.["type"];
|
|
223
|
-
if (columnType && columnType !== "array") {
|
|
224
|
-
throw new Error(`pg_jsonb_insert requires an array target. Column contains '${columnType}'. Use pg_jsonb_set for objects.`);
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JsonbInsertSchema.parse(params);
|
|
255
|
+
// Resolve table/column from optional aliases
|
|
256
|
+
const table = parsed.table ?? parsed.tableName;
|
|
257
|
+
const column = parsed.column ?? parsed.col;
|
|
258
|
+
if (!table || !column) {
|
|
259
|
+
return { success: false, error: "table and column are required" };
|
|
225
260
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
// Validate schema and build qualified table name
|
|
262
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
263
|
+
if (tableError)
|
|
264
|
+
return tableError;
|
|
265
|
+
// Normalize path - convert numeric segments to numbers for PostgreSQL
|
|
266
|
+
const path = normalizePathForInsert(parsed.path);
|
|
267
|
+
// Validate required 'where' parameter
|
|
268
|
+
if (!parsed.where || parsed.where.trim() === "") {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
error: 'pg_jsonb_insert requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
|
|
272
|
+
};
|
|
237
273
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
274
|
+
// Check for NULL columns first - jsonb_insert requires existing array context
|
|
275
|
+
const checkSql = `SELECT COUNT(*) as null_count FROM ${qualifiedTable} WHERE ${parsed.where} AND "${column}" IS NULL`;
|
|
276
|
+
const checkResult = await adapter.executeQuery(checkSql);
|
|
277
|
+
const nullCount = Number(checkResult.rows?.[0]?.["null_count"] ?? 0);
|
|
278
|
+
if (nullCount > 0) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
error: `pg_jsonb_insert cannot operate on NULL columns. Use pg_jsonb_set to initialize the column first: pg_jsonb_set({table: "${table}", column: "${column}", path: "myarray", value: [], where: "..."})`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
// Validate target path points to an array, not an object
|
|
285
|
+
// Get the parent path (one level up from where we're inserting)
|
|
286
|
+
const parentPath = path.slice(0, -1);
|
|
287
|
+
if (parentPath.length === 0) {
|
|
288
|
+
// Inserting at root level - check column type
|
|
289
|
+
const typeCheckSql = `SELECT jsonb_typeof("${column}") as type FROM ${qualifiedTable} WHERE ${parsed.where} LIMIT 1`;
|
|
290
|
+
const typeResult = await adapter.executeQuery(typeCheckSql);
|
|
291
|
+
const columnType = typeResult.rows?.[0]?.["type"];
|
|
292
|
+
if (columnType && columnType !== "array") {
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
error: `pg_jsonb_insert requires an array target. Column contains '${columnType}'. Use pg_jsonb_set for objects.`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Check the parent path type
|
|
301
|
+
const typeCheckSql = `SELECT jsonb_typeof("${column}" #> $1) as type FROM ${qualifiedTable} WHERE ${parsed.where} LIMIT 1`;
|
|
302
|
+
const parentPathStrings = parentPath.map((p) => String(p));
|
|
303
|
+
const typeResult = await adapter.executeQuery(typeCheckSql, [
|
|
304
|
+
parentPathStrings,
|
|
305
|
+
]);
|
|
306
|
+
const targetType = typeResult.rows?.[0]?.["type"];
|
|
307
|
+
if (targetType && targetType !== "array") {
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
error: `pg_jsonb_insert requires an array target. Path '${parentPathStrings.join(".")}' contains '${targetType}'. Use pg_jsonb_set for objects.`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_insert("${column}", $1, $2::jsonb, $3) WHERE ${parsed.where}`;
|
|
241
315
|
const result = await adapter.executeQuery(sql, [
|
|
242
316
|
path,
|
|
243
317
|
toJsonString(parsed.value),
|
|
@@ -246,16 +320,27 @@ export function createJsonbInsertTool(adapter) {
|
|
|
246
320
|
return { rowsAffected: result.rowsAffected };
|
|
247
321
|
}
|
|
248
322
|
catch (error) {
|
|
249
|
-
// Improve PostgreSQL error messages
|
|
323
|
+
// Improve specific PostgreSQL error messages
|
|
250
324
|
if (error instanceof Error &&
|
|
251
325
|
error.message.includes("cannot replace existing key")) {
|
|
252
|
-
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
error: `pg_jsonb_insert is for arrays only. For objects, use pg_jsonb_set. If updating an existing array element, use pg_jsonb_set.`,
|
|
329
|
+
};
|
|
253
330
|
}
|
|
254
331
|
if (error instanceof Error &&
|
|
255
332
|
error.message.includes("path element is not an integer")) {
|
|
256
|
-
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
error: `pg_jsonb_insert requires numeric index for array position. Use array format with number: ["tags", 0] not ["tags", "0"] or "tags.0"`,
|
|
336
|
+
};
|
|
257
337
|
}
|
|
258
|
-
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
error: formatPostgresError(error, {
|
|
341
|
+
tool: "pg_jsonb_insert",
|
|
342
|
+
}),
|
|
343
|
+
};
|
|
259
344
|
}
|
|
260
345
|
},
|
|
261
346
|
};
|
|
@@ -270,62 +355,72 @@ export function createJsonbDeleteTool(adapter) {
|
|
|
270
355
|
annotations: write("JSONB Delete"),
|
|
271
356
|
icons: getToolIcons("jsonb", write("JSONB Delete")),
|
|
272
357
|
handler: async (params, _context) => {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JsonbDeleteSchema.parse(params);
|
|
360
|
+
// Resolve table/column from optional aliases
|
|
361
|
+
const table = parsed.table ?? parsed.tableName;
|
|
362
|
+
const column = parsed.column ?? parsed.col;
|
|
363
|
+
if (!table || !column) {
|
|
364
|
+
return { success: false, error: "table and column are required" };
|
|
365
|
+
}
|
|
366
|
+
// Validate schema and build qualified table name
|
|
367
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
368
|
+
if (tableError)
|
|
369
|
+
return tableError;
|
|
370
|
+
// Validate required 'where' parameter
|
|
371
|
+
if (!parsed.where || parsed.where.trim() === "") {
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
error: 'pg_jsonb_delete requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
// Validate path is not empty
|
|
378
|
+
if (parsed.path === "" ||
|
|
379
|
+
(Array.isArray(parsed.path) && parsed.path.length === 0)) {
|
|
380
|
+
return {
|
|
381
|
+
success: false,
|
|
382
|
+
error: "pg_jsonb_delete requires a non-empty path. Provide a key name or path to delete.",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Determine if path should be treated as nested (array path) or single key
|
|
386
|
+
let pathForPostgres;
|
|
387
|
+
let useArrayOperator;
|
|
388
|
+
if (typeof parsed.path === "number") {
|
|
389
|
+
pathForPostgres = [String(parsed.path)];
|
|
390
|
+
useArrayOperator = true;
|
|
391
|
+
}
|
|
392
|
+
else if (Array.isArray(parsed.path)) {
|
|
393
|
+
pathForPostgres = normalizePathToArray(parsed.path);
|
|
394
|
+
useArrayOperator = true;
|
|
395
|
+
}
|
|
396
|
+
else if (parsed.path.includes(".")) {
|
|
397
|
+
pathForPostgres = parsed.path.split(".").filter((p) => p !== "");
|
|
398
|
+
useArrayOperator = true;
|
|
399
|
+
}
|
|
400
|
+
else if (/^\d+$/.test(parsed.path)) {
|
|
401
|
+
pathForPostgres = [parsed.path];
|
|
402
|
+
useArrayOperator = true;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
pathForPostgres = parsed.path;
|
|
406
|
+
useArrayOperator = false;
|
|
407
|
+
}
|
|
408
|
+
const pathExpr = useArrayOperator ? `#- $1` : `- $1`;
|
|
409
|
+
const sql = `UPDATE ${qualifiedTable} SET "${column}" = "${column}" ${pathExpr} WHERE ${parsed.where}`;
|
|
410
|
+
const result = await adapter.executeQuery(sql, [pathForPostgres]);
|
|
411
|
+
return {
|
|
412
|
+
rowsAffected: result.rowsAffected,
|
|
413
|
+
hint: "rowsAffected counts matched rows, not whether key existed",
|
|
414
|
+
};
|
|
316
415
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
416
|
+
catch (error) {
|
|
417
|
+
return {
|
|
418
|
+
success: false,
|
|
419
|
+
error: formatPostgresError(error, {
|
|
420
|
+
tool: "pg_jsonb_delete",
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
321
423
|
}
|
|
322
|
-
const pathExpr = useArrayOperator ? `#- $1` : `- $1`;
|
|
323
|
-
const sql = `UPDATE "${table}" SET "${column}" = "${column}" ${pathExpr} WHERE ${parsed.where}`;
|
|
324
|
-
const result = await adapter.executeQuery(sql, [pathForPostgres]);
|
|
325
|
-
return {
|
|
326
|
-
rowsAffected: result.rowsAffected,
|
|
327
|
-
hint: "rowsAffected counts matched rows, not whether key existed",
|
|
328
|
-
};
|
|
329
424
|
},
|
|
330
425
|
};
|
|
331
426
|
}
|
|
@@ -339,38 +434,52 @@ export function createJsonbContainsTool(adapter) {
|
|
|
339
434
|
annotations: readOnly("JSONB Contains"),
|
|
340
435
|
icons: getToolIcons("jsonb", readOnly("JSONB Contains")),
|
|
341
436
|
handler: async (params, _context) => {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
437
|
+
try {
|
|
438
|
+
const parsed = JsonbContainsSchema.parse(params);
|
|
439
|
+
// Resolve table/column from optional aliases
|
|
440
|
+
const table = parsed.table ?? parsed.tableName;
|
|
441
|
+
const column = parsed.column ?? parsed.col;
|
|
442
|
+
if (!table || !column) {
|
|
443
|
+
return { success: false, error: "table and column are required" };
|
|
444
|
+
}
|
|
445
|
+
// Validate schema and build qualified table name
|
|
446
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
447
|
+
if (tableError)
|
|
448
|
+
return tableError;
|
|
449
|
+
const { select, where } = parsed;
|
|
450
|
+
// Parse JSON string values from MCP clients
|
|
451
|
+
const value = parseJsonbValue(parsed.value);
|
|
452
|
+
const selectCols = select !== undefined && select.length > 0
|
|
453
|
+
? select.map((c) => `"${c}"`).join(", ")
|
|
454
|
+
: "*";
|
|
455
|
+
// Build WHERE clause combining containment check with optional filter
|
|
456
|
+
const containsClause = `"${column}" @> $1::jsonb`;
|
|
457
|
+
const whereClause = where ? ` AND ${where}` : "";
|
|
458
|
+
const sql = `SELECT ${selectCols} FROM ${qualifiedTable} WHERE ${containsClause}${whereClause}`;
|
|
459
|
+
const result = await adapter.executeQuery(sql, [toJsonString(value)]);
|
|
460
|
+
// Warn if empty object was passed (matches all rows)
|
|
461
|
+
const isEmptyObject = typeof value === "object" &&
|
|
462
|
+
value !== null &&
|
|
463
|
+
!Array.isArray(value) &&
|
|
464
|
+
Object.keys(value).length === 0;
|
|
465
|
+
const response = {
|
|
466
|
+
rows: result.rows,
|
|
467
|
+
count: result.rows?.length ?? 0,
|
|
468
|
+
};
|
|
469
|
+
if (isEmptyObject) {
|
|
470
|
+
response.warning =
|
|
471
|
+
"Empty {} matches ALL rows - this is PostgreSQL containment semantics";
|
|
472
|
+
}
|
|
473
|
+
return response;
|
|
348
474
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const containsClause = `"${column}" @> $1::jsonb`;
|
|
357
|
-
const whereClause = where ? ` AND ${where}` : "";
|
|
358
|
-
const sql = `SELECT ${selectCols} FROM "${table}" WHERE ${containsClause}${whereClause}`;
|
|
359
|
-
const result = await adapter.executeQuery(sql, [toJsonString(value)]);
|
|
360
|
-
// Warn if empty object was passed (matches all rows)
|
|
361
|
-
const isEmptyObject = typeof value === "object" &&
|
|
362
|
-
value !== null &&
|
|
363
|
-
!Array.isArray(value) &&
|
|
364
|
-
Object.keys(value).length === 0;
|
|
365
|
-
const response = {
|
|
366
|
-
rows: result.rows,
|
|
367
|
-
count: result.rows?.length ?? 0,
|
|
368
|
-
};
|
|
369
|
-
if (isEmptyObject) {
|
|
370
|
-
response.warning =
|
|
371
|
-
"Empty {} matches ALL rows - this is PostgreSQL containment semantics";
|
|
475
|
+
catch (error) {
|
|
476
|
+
return {
|
|
477
|
+
success: false,
|
|
478
|
+
error: formatPostgresError(error, {
|
|
479
|
+
tool: "pg_jsonb_contains",
|
|
480
|
+
}),
|
|
481
|
+
};
|
|
372
482
|
}
|
|
373
|
-
return response;
|
|
374
483
|
},
|
|
375
484
|
};
|
|
376
485
|
}
|
|
@@ -384,20 +493,43 @@ export function createJsonbPathQueryTool(adapter) {
|
|
|
384
493
|
annotations: readOnly("JSONB Path Query"),
|
|
385
494
|
icons: getToolIcons("jsonb", readOnly("JSONB Path Query")),
|
|
386
495
|
handler: async (params, _context) => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
496
|
+
try {
|
|
497
|
+
const parsed = JsonbPathQuerySchema.parse(params);
|
|
498
|
+
// Resolve table/column from optional aliases
|
|
499
|
+
const table = parsed.table ?? parsed.tableName;
|
|
500
|
+
const column = parsed.column ?? parsed.col;
|
|
501
|
+
if (!table || !column) {
|
|
502
|
+
return { success: false, error: "table and column are required" };
|
|
503
|
+
}
|
|
504
|
+
// Validate schema and build qualified table name
|
|
505
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
506
|
+
if (tableError)
|
|
507
|
+
return tableError;
|
|
508
|
+
const { path, vars, where } = parsed;
|
|
509
|
+
const whereClause = where ? ` WHERE ${where}` : "";
|
|
510
|
+
const varsJson = vars ? JSON.stringify(vars) : "{}";
|
|
511
|
+
const sql = `SELECT jsonb_path_query("${column}", $1::jsonpath, $2::jsonb) as result FROM ${qualifiedTable}${whereClause}`;
|
|
512
|
+
const result = await adapter.executeQuery(sql, [path, varsJson]);
|
|
513
|
+
const results = result.rows?.map((r) => r["result"]);
|
|
514
|
+
return { results, count: results?.length ?? 0 };
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
// JSONPath-specific: invalid syntax
|
|
518
|
+
if (error instanceof Error &&
|
|
519
|
+
/syntax error/i.test(error.message) &&
|
|
520
|
+
/jsonpath/i.test(error.message)) {
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
error: `Invalid JSONPath syntax. Use $.key, $.array[*], or $.* ? (@.field > 10) syntax.`,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
error: formatPostgresError(error, {
|
|
529
|
+
tool: "pg_jsonb_path_query",
|
|
530
|
+
}),
|
|
531
|
+
};
|
|
393
532
|
}
|
|
394
|
-
const { path, vars, where } = parsed;
|
|
395
|
-
const whereClause = where ? ` WHERE ${where}` : "";
|
|
396
|
-
const varsJson = vars ? JSON.stringify(vars) : "{}";
|
|
397
|
-
const sql = `SELECT jsonb_path_query("${column}", $1::jsonpath, $2::jsonb) as result FROM "${table}"${whereClause}`;
|
|
398
|
-
const result = await adapter.executeQuery(sql, [path, varsJson]);
|
|
399
|
-
const results = result.rows?.map((r) => r["result"]);
|
|
400
|
-
return { results, count: results?.length ?? 0 };
|
|
401
533
|
},
|
|
402
534
|
};
|
|
403
535
|
}
|
|
@@ -433,67 +565,77 @@ export function createJsonbAggTool(adapter) {
|
|
|
433
565
|
annotations: readOnly("JSONB Aggregate"),
|
|
434
566
|
icons: getToolIcons("jsonb", readOnly("JSONB Aggregate")),
|
|
435
567
|
handler: async (params, _context) => {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
568
|
+
try {
|
|
569
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, filter→where)
|
|
570
|
+
const parsed = JsonbAggSchema.parse(params);
|
|
571
|
+
const table = parsed.table;
|
|
572
|
+
if (!table) {
|
|
573
|
+
return { success: false, error: "table is required" };
|
|
574
|
+
}
|
|
575
|
+
// Validate schema and build qualified table name
|
|
576
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
577
|
+
if (tableError)
|
|
578
|
+
return tableError;
|
|
579
|
+
// Build select expression with proper alias handling
|
|
580
|
+
let selectExpr;
|
|
581
|
+
if (parsed.select !== undefined && parsed.select.length > 0) {
|
|
582
|
+
const selectParts = parsed.select.map((item) => {
|
|
583
|
+
const { expr, alias } = parseSelectAlias(item);
|
|
584
|
+
const needsQuote = !expr.includes("->") &&
|
|
585
|
+
!expr.includes("(") &&
|
|
586
|
+
!expr.includes("::") &&
|
|
587
|
+
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(expr);
|
|
588
|
+
const exprStr = needsQuote ? `"${expr}"` : expr;
|
|
589
|
+
return `'${alias}', ${exprStr}`;
|
|
590
|
+
});
|
|
591
|
+
selectExpr = `jsonb_build_object(${selectParts.join(", ")})`;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
selectExpr = "to_jsonb(t.*)";
|
|
595
|
+
}
|
|
596
|
+
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
597
|
+
const orderByClause = parsed.orderBy
|
|
598
|
+
? ` ORDER BY ${parsed.orderBy}`
|
|
599
|
+
: "";
|
|
600
|
+
const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
|
|
601
|
+
const hasJsonbOperator = parsed.groupBy?.includes("->") ?? false;
|
|
602
|
+
if (parsed.groupBy) {
|
|
603
|
+
const groupExpr = hasJsonbOperator
|
|
604
|
+
? parsed.groupBy
|
|
605
|
+
: `"${parsed.groupBy}"`;
|
|
606
|
+
const groupClause = ` GROUP BY ${groupExpr}`;
|
|
607
|
+
const aggOrderBy = parsed.orderBy
|
|
608
|
+
? ` ORDER BY ${parsed.orderBy}`
|
|
609
|
+
: "";
|
|
610
|
+
const sql = `SELECT ${groupExpr} as group_key, jsonb_agg(${selectExpr}${aggOrderBy}) as items FROM ${qualifiedTable} t${whereClause}${groupClause}${limitClause}`;
|
|
611
|
+
const result = await adapter.executeQuery(sql);
|
|
612
|
+
return {
|
|
613
|
+
result: result.rows,
|
|
614
|
+
count: result.rows?.length ?? 0,
|
|
615
|
+
grouped: true,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
const innerSql = `SELECT * FROM ${qualifiedTable} t${whereClause}${orderByClause}${limitClause}`;
|
|
620
|
+
const sql = `SELECT jsonb_agg(${selectExpr.replace(/\bt\./g, "sub.")}) as result FROM (${innerSql}) sub`;
|
|
621
|
+
const result = await adapter.executeQuery(sql);
|
|
622
|
+
const arr = result.rows?.[0]?.["result"] ?? [];
|
|
623
|
+
const count = Array.isArray(arr) ? arr.length : 0;
|
|
624
|
+
const response = { result: arr, count, grouped: false };
|
|
625
|
+
if (count === 0) {
|
|
626
|
+
response.hint = "No rows matched - returns empty array []";
|
|
627
|
+
}
|
|
628
|
+
return response;
|
|
629
|
+
}
|
|
460
630
|
}
|
|
461
|
-
|
|
462
|
-
const orderByClause = parsed.orderBy ? ` ORDER BY ${parsed.orderBy}` : "";
|
|
463
|
-
const limitClause = parsed.limit !== undefined ? ` LIMIT ${String(parsed.limit)}` : "";
|
|
464
|
-
// Support raw JSONB expressions (containing -> or ->> operators) without quoting
|
|
465
|
-
const hasJsonbOperator = parsed.groupBy?.includes("->") ?? false;
|
|
466
|
-
if (parsed.groupBy) {
|
|
467
|
-
// Return all groups with their aggregated items
|
|
468
|
-
// For grouped queries, we use a subquery to apply ordering before aggregation
|
|
469
|
-
const groupExpr = hasJsonbOperator
|
|
470
|
-
? parsed.groupBy
|
|
471
|
-
: `"${parsed.groupBy}"`;
|
|
472
|
-
const groupClause = ` GROUP BY ${groupExpr}`;
|
|
473
|
-
// Apply ordering within each group using ORDER BY inside jsonb_agg
|
|
474
|
-
const aggOrderBy = parsed.orderBy ? ` ORDER BY ${parsed.orderBy}` : "";
|
|
475
|
-
const sql = `SELECT ${groupExpr} as group_key, jsonb_agg(${selectExpr}${aggOrderBy}) as items FROM "${table}" t${whereClause}${groupClause}${limitClause}`;
|
|
476
|
-
const result = await adapter.executeQuery(sql);
|
|
477
|
-
// Return grouped result with group_key and items per group
|
|
631
|
+
catch (error) {
|
|
478
632
|
return {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
633
|
+
success: false,
|
|
634
|
+
error: formatPostgresError(error, {
|
|
635
|
+
tool: "pg_jsonb_agg",
|
|
636
|
+
}),
|
|
482
637
|
};
|
|
483
638
|
}
|
|
484
|
-
else {
|
|
485
|
-
// For non-grouped, use subquery to apply limit/order before aggregation
|
|
486
|
-
const innerSql = `SELECT * FROM "${table}" t${whereClause}${orderByClause}${limitClause}`;
|
|
487
|
-
const sql = `SELECT jsonb_agg(${selectExpr.replace(/\bt\./g, "sub.")}) as result FROM (${innerSql}) sub`;
|
|
488
|
-
const result = await adapter.executeQuery(sql);
|
|
489
|
-
const arr = result.rows?.[0]?.["result"] ?? [];
|
|
490
|
-
const count = Array.isArray(arr) ? arr.length : 0;
|
|
491
|
-
const response = { result: arr, count, grouped: false };
|
|
492
|
-
if (count === 0) {
|
|
493
|
-
response.hint = "No rows matched - returns empty array []";
|
|
494
|
-
}
|
|
495
|
-
return response;
|
|
496
|
-
}
|
|
497
639
|
},
|
|
498
640
|
};
|
|
499
641
|
}
|
|
@@ -526,22 +668,32 @@ export function createJsonbObjectTool(adapter) {
|
|
|
526
668
|
annotations: readOnly("JSONB Object"),
|
|
527
669
|
icons: getToolIcons("jsonb", readOnly("JSONB Object")),
|
|
528
670
|
handler: async (params, _context) => {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
671
|
+
try {
|
|
672
|
+
// Parse the input
|
|
673
|
+
const parsed = JsonbObjectSchema.parse(params);
|
|
674
|
+
// Support multiple parameter names: data, object, pairs (in priority order)
|
|
675
|
+
const pairs = parsed.data ?? parsed.object ?? parsed.pairs ?? {};
|
|
676
|
+
const entries = Object.entries(pairs);
|
|
677
|
+
// Handle empty pairs - return empty object
|
|
678
|
+
if (entries.length === 0) {
|
|
679
|
+
return { object: {} };
|
|
680
|
+
}
|
|
681
|
+
const args = entries.flatMap(([k, v]) => [k, toJsonString(v)]);
|
|
682
|
+
const placeholders = entries
|
|
683
|
+
.map((_, i) => `$${String(i * 2 + 1)}::text, $${String(i * 2 + 2)}::jsonb`)
|
|
684
|
+
.join(", ");
|
|
685
|
+
const sql = `SELECT jsonb_build_object(${placeholders}) as result`;
|
|
686
|
+
const result = await adapter.executeQuery(sql, args);
|
|
687
|
+
return { object: result.rows?.[0]?.["result"] ?? {} };
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
error: formatPostgresError(error, {
|
|
693
|
+
tool: "pg_jsonb_object",
|
|
694
|
+
}),
|
|
695
|
+
};
|
|
537
696
|
}
|
|
538
|
-
const args = entries.flatMap(([k, v]) => [k, toJsonString(v)]);
|
|
539
|
-
const placeholders = entries
|
|
540
|
-
.map((_, i) => `$${String(i * 2 + 1)}::text, $${String(i * 2 + 2)}::jsonb`)
|
|
541
|
-
.join(", ");
|
|
542
|
-
const sql = `SELECT jsonb_build_object(${placeholders}) as result`;
|
|
543
|
-
const result = await adapter.executeQuery(sql, args);
|
|
544
|
-
return { object: result.rows?.[0]?.["result"] ?? {} };
|
|
545
697
|
},
|
|
546
698
|
};
|
|
547
699
|
}
|
|
@@ -567,18 +719,28 @@ export function createJsonbArrayTool(adapter) {
|
|
|
567
719
|
annotations: readOnly("JSONB Array"),
|
|
568
720
|
icons: getToolIcons("jsonb", readOnly("JSONB Array")),
|
|
569
721
|
handler: async (params, _context) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
722
|
+
try {
|
|
723
|
+
const parsed = params;
|
|
724
|
+
// Support both 'values' and 'elements' parameter names
|
|
725
|
+
const values = parsed.values ?? parsed.elements ?? [];
|
|
726
|
+
if (values.length === 0) {
|
|
727
|
+
return { array: [] };
|
|
728
|
+
}
|
|
729
|
+
const placeholders = values
|
|
730
|
+
.map((_, i) => `$${String(i + 1)}::jsonb`)
|
|
731
|
+
.join(", ");
|
|
732
|
+
const sql = `SELECT jsonb_build_array(${placeholders}) as result`;
|
|
733
|
+
const result = await adapter.executeQuery(sql, values.map((v) => toJsonString(v)));
|
|
734
|
+
return { array: result.rows?.[0]?.["result"] };
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
error: formatPostgresError(error, {
|
|
740
|
+
tool: "pg_jsonb_array",
|
|
741
|
+
}),
|
|
742
|
+
};
|
|
575
743
|
}
|
|
576
|
-
const placeholders = values
|
|
577
|
-
.map((_, i) => `$${String(i + 1)}::jsonb`)
|
|
578
|
-
.join(", ");
|
|
579
|
-
const sql = `SELECT jsonb_build_array(${placeholders}) as result`;
|
|
580
|
-
const result = await adapter.executeQuery(sql, values.map((v) => toJsonString(v)));
|
|
581
|
-
return { array: result.rows?.[0]?.["result"] };
|
|
582
744
|
},
|
|
583
745
|
};
|
|
584
746
|
}
|
|
@@ -592,16 +754,20 @@ export function createJsonbKeysTool(adapter) {
|
|
|
592
754
|
annotations: readOnly("JSONB Keys"),
|
|
593
755
|
icons: getToolIcons("jsonb", readOnly("JSONB Keys")),
|
|
594
756
|
handler: async (params, _context) => {
|
|
595
|
-
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
596
|
-
const parsed = JsonbKeysSchema.parse(params);
|
|
597
|
-
const table = parsed.table;
|
|
598
|
-
const column = parsed.column;
|
|
599
|
-
if (!table || !column) {
|
|
600
|
-
throw new Error("table and column are required");
|
|
601
|
-
}
|
|
602
|
-
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
603
|
-
const sql = `SELECT DISTINCT jsonb_object_keys("${column}") as key FROM "${table}"${whereClause}`;
|
|
604
757
|
try {
|
|
758
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
759
|
+
const parsed = JsonbKeysSchema.parse(params);
|
|
760
|
+
const table = parsed.table;
|
|
761
|
+
const column = parsed.column;
|
|
762
|
+
if (!table || !column) {
|
|
763
|
+
return { success: false, error: "table and column are required" };
|
|
764
|
+
}
|
|
765
|
+
// Validate schema and build qualified table name
|
|
766
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
767
|
+
if (tableError)
|
|
768
|
+
return tableError;
|
|
769
|
+
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
770
|
+
const sql = `SELECT DISTINCT jsonb_object_keys("${column}") as key FROM ${qualifiedTable}${whereClause}`;
|
|
605
771
|
const result = await adapter.executeQuery(sql);
|
|
606
772
|
const keys = result.rows?.map((r) => r["key"]);
|
|
607
773
|
return {
|
|
@@ -614,9 +780,17 @@ export function createJsonbKeysTool(adapter) {
|
|
|
614
780
|
// Improve error for array columns
|
|
615
781
|
if (error instanceof Error &&
|
|
616
782
|
error.message.includes("cannot call jsonb_object_keys")) {
|
|
617
|
-
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
error: `pg_jsonb_keys requires object columns. For array columns, use pg_jsonb_normalize with mode: 'array'.`,
|
|
786
|
+
};
|
|
618
787
|
}
|
|
619
|
-
|
|
788
|
+
return {
|
|
789
|
+
success: false,
|
|
790
|
+
error: formatPostgresError(error, {
|
|
791
|
+
tool: "pg_jsonb_keys",
|
|
792
|
+
}),
|
|
793
|
+
};
|
|
620
794
|
}
|
|
621
795
|
},
|
|
622
796
|
};
|
|
@@ -631,32 +805,63 @@ export function createJsonbStripNullsTool(adapter) {
|
|
|
631
805
|
annotations: write("JSONB Strip Nulls"),
|
|
632
806
|
icons: getToolIcons("jsonb", write("JSONB Strip Nulls")),
|
|
633
807
|
handler: async (params, _context) => {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
808
|
+
try {
|
|
809
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
810
|
+
// Wrap in try-catch to intercept Zod .refine() errors (e.g., missing WHERE)
|
|
811
|
+
let parsed;
|
|
812
|
+
try {
|
|
813
|
+
parsed = JsonbStripNullsSchema.parse(params);
|
|
814
|
+
}
|
|
815
|
+
catch (error) {
|
|
816
|
+
if (error instanceof ZodError) {
|
|
817
|
+
const messages = error.issues.map((i) => i.message).join("; ");
|
|
818
|
+
return {
|
|
819
|
+
success: false,
|
|
820
|
+
error: `pg_jsonb_strip_nulls validation error: ${messages}`,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
const table = parsed.table;
|
|
826
|
+
const column = parsed.column;
|
|
827
|
+
const whereClause = parsed.where;
|
|
828
|
+
if (!table || !column) {
|
|
829
|
+
return { success: false, error: "table and column are required" };
|
|
830
|
+
}
|
|
831
|
+
// Validate schema and build qualified table name
|
|
832
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
833
|
+
if (tableError)
|
|
834
|
+
return tableError;
|
|
835
|
+
// Validate required 'where' parameter before SQL execution
|
|
836
|
+
if (!whereClause || whereClause.trim() === "") {
|
|
837
|
+
return {
|
|
838
|
+
success: false,
|
|
839
|
+
error: 'pg_jsonb_strip_nulls requires a WHERE clause to identify rows to update. Example: where: "id = 1"',
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (parsed.preview === true) {
|
|
843
|
+
// Preview mode - show before/after without modifying
|
|
844
|
+
const previewSql = `SELECT "${column}" as before, jsonb_strip_nulls("${column}") as after FROM ${qualifiedTable} WHERE ${whereClause}`;
|
|
845
|
+
const result = await adapter.executeQuery(previewSql);
|
|
846
|
+
return {
|
|
847
|
+
preview: true,
|
|
848
|
+
rows: result.rows,
|
|
849
|
+
count: result.rows?.length ?? 0,
|
|
850
|
+
hint: "No changes made - preview only",
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
const sql = `UPDATE ${qualifiedTable} SET "${column}" = jsonb_strip_nulls("${column}") WHERE ${whereClause}`;
|
|
854
|
+
const result = await adapter.executeQuery(sql);
|
|
855
|
+
return { rowsAffected: result.rowsAffected };
|
|
645
856
|
}
|
|
646
|
-
|
|
647
|
-
// Preview mode - show before/after without modifying
|
|
648
|
-
const previewSql = `SELECT "${column}" as before, jsonb_strip_nulls("${column}") as after FROM "${table}" WHERE ${whereClause}`;
|
|
649
|
-
const result = await adapter.executeQuery(previewSql);
|
|
857
|
+
catch (error) {
|
|
650
858
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
859
|
+
success: false,
|
|
860
|
+
error: formatPostgresError(error, {
|
|
861
|
+
tool: "pg_jsonb_strip_nulls",
|
|
862
|
+
}),
|
|
655
863
|
};
|
|
656
864
|
}
|
|
657
|
-
const sql = `UPDATE "${table}" SET "${column}" = jsonb_strip_nulls("${column}") WHERE ${whereClause}`;
|
|
658
|
-
const result = await adapter.executeQuery(sql);
|
|
659
|
-
return { rowsAffected: result.rowsAffected };
|
|
660
865
|
},
|
|
661
866
|
};
|
|
662
867
|
}
|
|
@@ -670,26 +875,40 @@ export function createJsonbTypeofTool(adapter) {
|
|
|
670
875
|
annotations: readOnly("JSONB Typeof"),
|
|
671
876
|
icons: getToolIcons("jsonb", readOnly("JSONB Typeof")),
|
|
672
877
|
handler: async (params, _context) => {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
878
|
+
try {
|
|
879
|
+
// Parse with preprocess schema to resolve aliases (tableName→table, col→column, filter→where)
|
|
880
|
+
const parsed = JsonbTypeofSchema.parse(params);
|
|
881
|
+
const table = parsed.table;
|
|
882
|
+
const column = parsed.column;
|
|
883
|
+
if (!table || !column) {
|
|
884
|
+
return { success: false, error: "table and column are required" };
|
|
885
|
+
}
|
|
886
|
+
// Validate schema and build qualified table name
|
|
887
|
+
const [qualifiedTable, tableError] = await resolveJsonbTable(adapter, table, parsed.schema);
|
|
888
|
+
if (tableError)
|
|
889
|
+
return tableError;
|
|
890
|
+
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
891
|
+
// Normalize path to array format (accepts both string and array)
|
|
892
|
+
const pathArray = parsed.path !== undefined
|
|
893
|
+
? normalizePathToArray(parsed.path)
|
|
894
|
+
: undefined;
|
|
895
|
+
const pathExpr = pathArray !== undefined ? ` #> $1` : "";
|
|
896
|
+
// Include column IS NULL check to disambiguate NULL column vs null path result
|
|
897
|
+
const sql = `SELECT jsonb_typeof("${column}"${pathExpr}) as type, ("${column}" IS NULL) as column_null FROM ${qualifiedTable}${whereClause}`;
|
|
898
|
+
const queryParams = pathArray ? [pathArray] : [];
|
|
899
|
+
const result = await adapter.executeQuery(sql, queryParams);
|
|
900
|
+
const types = result.rows?.map((r) => r["type"]);
|
|
901
|
+
const columnNull = result.rows?.some((r) => r["column_null"] === true) ?? false;
|
|
902
|
+
return { types, count: types?.length ?? 0, columnNull };
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
error: formatPostgresError(error, {
|
|
908
|
+
tool: "pg_jsonb_typeof",
|
|
909
|
+
}),
|
|
910
|
+
};
|
|
679
911
|
}
|
|
680
|
-
const whereClause = parsed.where ? ` WHERE ${parsed.where}` : "";
|
|
681
|
-
// Normalize path to array format (accepts both string and array)
|
|
682
|
-
const pathArray = parsed.path !== undefined
|
|
683
|
-
? normalizePathToArray(parsed.path)
|
|
684
|
-
: undefined;
|
|
685
|
-
const pathExpr = pathArray !== undefined ? ` #> $1` : "";
|
|
686
|
-
// Include column IS NULL check to disambiguate NULL column vs null path result
|
|
687
|
-
const sql = `SELECT jsonb_typeof("${column}"${pathExpr}) as type, ("${column}" IS NULL) as column_null FROM "${table}"${whereClause}`;
|
|
688
|
-
const queryParams = pathArray ? [pathArray] : [];
|
|
689
|
-
const result = await adapter.executeQuery(sql, queryParams);
|
|
690
|
-
const types = result.rows?.map((r) => r["type"]);
|
|
691
|
-
const columnNull = result.rows?.some((r) => r["column_null"] === true) ?? false;
|
|
692
|
-
return { types, count: types?.length ?? 0, columnNull };
|
|
693
912
|
},
|
|
694
913
|
};
|
|
695
914
|
}
|