db-mcp 1.0.1
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/LICENSE +21 -0
- package/README.md +860 -0
- package/dist/adapters/DatabaseAdapter.d.ts +141 -0
- package/dist/adapters/DatabaseAdapter.d.ts.map +1 -0
- package/dist/adapters/DatabaseAdapter.js +131 -0
- package/dist/adapters/DatabaseAdapter.js.map +1 -0
- package/dist/adapters/sqlite/SchemaManager.d.ts +58 -0
- package/dist/adapters/sqlite/SchemaManager.d.ts.map +1 -0
- package/dist/adapters/sqlite/SchemaManager.js +187 -0
- package/dist/adapters/sqlite/SchemaManager.js.map +1 -0
- package/dist/adapters/sqlite/SqliteAdapter.d.ts +161 -0
- package/dist/adapters/sqlite/SqliteAdapter.d.ts.map +1 -0
- package/dist/adapters/sqlite/SqliteAdapter.js +741 -0
- package/dist/adapters/sqlite/SqliteAdapter.js.map +1 -0
- package/dist/adapters/sqlite/index.d.ts +9 -0
- package/dist/adapters/sqlite/index.d.ts.map +1 -0
- package/dist/adapters/sqlite/index.js +8 -0
- package/dist/adapters/sqlite/index.js.map +1 -0
- package/dist/adapters/sqlite/json-utils.d.ts +100 -0
- package/dist/adapters/sqlite/json-utils.d.ts.map +1 -0
- package/dist/adapters/sqlite/json-utils.js +274 -0
- package/dist/adapters/sqlite/json-utils.js.map +1 -0
- package/dist/adapters/sqlite/output-schemas.d.ts +1187 -0
- package/dist/adapters/sqlite/output-schemas.d.ts.map +1 -0
- package/dist/adapters/sqlite/output-schemas.js +1337 -0
- package/dist/adapters/sqlite/output-schemas.js.map +1 -0
- package/dist/adapters/sqlite/prompts.d.ts +13 -0
- package/dist/adapters/sqlite/prompts.d.ts.map +1 -0
- package/dist/adapters/sqlite/prompts.js +605 -0
- package/dist/adapters/sqlite/prompts.js.map +1 -0
- package/dist/adapters/sqlite/resources.d.ts +13 -0
- package/dist/adapters/sqlite/resources.d.ts.map +1 -0
- package/dist/adapters/sqlite/resources.js +251 -0
- package/dist/adapters/sqlite/resources.js.map +1 -0
- package/dist/adapters/sqlite/tools/admin.d.ts +14 -0
- package/dist/adapters/sqlite/tools/admin.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/admin.js +788 -0
- package/dist/adapters/sqlite/tools/admin.js.map +1 -0
- package/dist/adapters/sqlite/tools/core.d.ts +25 -0
- package/dist/adapters/sqlite/tools/core.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/core.js +359 -0
- package/dist/adapters/sqlite/tools/core.js.map +1 -0
- package/dist/adapters/sqlite/tools/fts.d.ts +13 -0
- package/dist/adapters/sqlite/tools/fts.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/fts.js +347 -0
- package/dist/adapters/sqlite/tools/fts.js.map +1 -0
- package/dist/adapters/sqlite/tools/geo.d.ts +14 -0
- package/dist/adapters/sqlite/tools/geo.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/geo.js +252 -0
- package/dist/adapters/sqlite/tools/geo.js.map +1 -0
- package/dist/adapters/sqlite/tools/index.d.ts +30 -0
- package/dist/adapters/sqlite/tools/index.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/index.js +61 -0
- package/dist/adapters/sqlite/tools/index.js.map +1 -0
- package/dist/adapters/sqlite/tools/json-helpers.d.ts +14 -0
- package/dist/adapters/sqlite/tools/json-helpers.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/json-helpers.js +477 -0
- package/dist/adapters/sqlite/tools/json-helpers.js.map +1 -0
- package/dist/adapters/sqlite/tools/json-operations.d.ts +14 -0
- package/dist/adapters/sqlite/tools/json-operations.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/json-operations.js +839 -0
- package/dist/adapters/sqlite/tools/json-operations.js.map +1 -0
- package/dist/adapters/sqlite/tools/stats.d.ts +15 -0
- package/dist/adapters/sqlite/tools/stats.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/stats.js +1219 -0
- package/dist/adapters/sqlite/tools/stats.js.map +1 -0
- package/dist/adapters/sqlite/tools/text.d.ts +14 -0
- package/dist/adapters/sqlite/tools/text.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/text.js +1141 -0
- package/dist/adapters/sqlite/tools/text.js.map +1 -0
- package/dist/adapters/sqlite/tools/vector.d.ts +14 -0
- package/dist/adapters/sqlite/tools/vector.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/vector.js +613 -0
- package/dist/adapters/sqlite/tools/vector.js.map +1 -0
- package/dist/adapters/sqlite/tools/virtual.d.ts +13 -0
- package/dist/adapters/sqlite/tools/virtual.d.ts.map +1 -0
- package/dist/adapters/sqlite/tools/virtual.js +930 -0
- package/dist/adapters/sqlite/tools/virtual.js.map +1 -0
- package/dist/adapters/sqlite/types.d.ts +207 -0
- package/dist/adapters/sqlite/types.d.ts.map +1 -0
- package/dist/adapters/sqlite/types.js +186 -0
- package/dist/adapters/sqlite/types.js.map +1 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts +163 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js +748 -0
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js.map +1 -0
- package/dist/adapters/sqlite-native/index.d.ts +11 -0
- package/dist/adapters/sqlite-native/index.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/index.js +11 -0
- package/dist/adapters/sqlite-native/index.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts +19 -0
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/spatialite.js +628 -0
- package/dist/adapters/sqlite-native/tools/spatialite.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/transactions.d.ts +12 -0
- package/dist/adapters/sqlite-native/tools/transactions.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/transactions.js +255 -0
- package/dist/adapters/sqlite-native/tools/transactions.js.map +1 -0
- package/dist/adapters/sqlite-native/tools/window.d.ts +12 -0
- package/dist/adapters/sqlite-native/tools/window.d.ts.map +1 -0
- package/dist/adapters/sqlite-native/tools/window.js +370 -0
- package/dist/adapters/sqlite-native/tools/window.js.map +1 -0
- package/dist/auth/AuthorizationServerDiscovery.d.ts +90 -0
- package/dist/auth/AuthorizationServerDiscovery.d.ts.map +1 -0
- package/dist/auth/AuthorizationServerDiscovery.js +204 -0
- package/dist/auth/AuthorizationServerDiscovery.js.map +1 -0
- package/dist/auth/OAuthResourceServer.d.ts +65 -0
- package/dist/auth/OAuthResourceServer.d.ts.map +1 -0
- package/dist/auth/OAuthResourceServer.js +121 -0
- package/dist/auth/OAuthResourceServer.js.map +1 -0
- package/dist/auth/TokenValidator.d.ts +60 -0
- package/dist/auth/TokenValidator.d.ts.map +1 -0
- package/dist/auth/TokenValidator.js +235 -0
- package/dist/auth/TokenValidator.js.map +1 -0
- package/dist/auth/errors.d.ts +74 -0
- package/dist/auth/errors.d.ts.map +1 -0
- package/dist/auth/errors.js +133 -0
- package/dist/auth/errors.js.map +1 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +81 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +291 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/scopes.d.ts +136 -0
- package/dist/auth/scopes.d.ts.map +1 -0
- package/dist/auth/scopes.js +349 -0
- package/dist/auth/scopes.js.map +1 -0
- package/dist/auth/types.d.ts +257 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +8 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +236 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants/ServerInstructions.d.ts +45 -0
- package/dist/constants/ServerInstructions.d.ts.map +1 -0
- package/dist/constants/ServerInstructions.js +356 -0
- package/dist/constants/ServerInstructions.js.map +1 -0
- package/dist/filtering/ToolConstants.d.ts +34 -0
- package/dist/filtering/ToolConstants.d.ts.map +1 -0
- package/dist/filtering/ToolConstants.js +174 -0
- package/dist/filtering/ToolConstants.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +82 -0
- package/dist/filtering/ToolFilter.d.ts.map +1 -0
- package/dist/filtering/ToolFilter.js +296 -0
- package/dist/filtering/ToolFilter.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/server/McpServer.d.ts +61 -0
- package/dist/server/McpServer.d.ts.map +1 -0
- package/dist/server/McpServer.js +270 -0
- package/dist/server/McpServer.js.map +1 -0
- package/dist/transports/http.d.ts +134 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +516 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/index.d.ts +5 -0
- package/dist/transports/index.d.ts.map +1 -0
- package/dist/transports/index.js +5 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/types/index.d.ts +380 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +68 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/annotations.d.ts +44 -0
- package/dist/utils/annotations.d.ts.map +1 -0
- package/dist/utils/annotations.js +77 -0
- package/dist/utils/annotations.js.map +1 -0
- package/dist/utils/errors.d.ts +155 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +329 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/identifiers.d.ts +121 -0
- package/dist/utils/identifiers.d.ts.map +1 -0
- package/dist/utils/identifiers.js +319 -0
- package/dist/utils/identifiers.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/insightsManager.d.ts +39 -0
- package/dist/utils/insightsManager.d.ts.map +1 -0
- package/dist/utils/insightsManager.js +63 -0
- package/dist/utils/insightsManager.js.map +1 -0
- package/dist/utils/logger.d.ts +189 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +394 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/progress-utils.d.ts +54 -0
- package/dist/utils/progress-utils.d.ts.map +1 -0
- package/dist/utils/progress-utils.js +74 -0
- package/dist/utils/progress-utils.js.map +1 -0
- package/dist/utils/resourceAnnotations.d.ts +36 -0
- package/dist/utils/resourceAnnotations.d.ts.map +1 -0
- package/dist/utils/resourceAnnotations.js +57 -0
- package/dist/utils/resourceAnnotations.js.map +1 -0
- package/dist/utils/where-clause.d.ts +41 -0
- package/dist/utils/where-clause.d.ts.map +1 -0
- package/dist/utils/where-clause.js +116 -0
- package/dist/utils/where-clause.js.map +1 -0
- package/package.json +83 -0
- package/server.json +53 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite JSON Operation Tools
|
|
3
|
+
*
|
|
4
|
+
* Low-level JSON functions wrapping SQLite's JSON1 extension:
|
|
5
|
+
* validate, extract, set, remove, type, array/object operations, etc.
|
|
6
|
+
* 12 tools total.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readOnly, write } from "../../../utils/annotations.js";
|
|
10
|
+
import { sanitizeIdentifier } from "../../../utils/index.js";
|
|
11
|
+
import { ValidateJsonSchema, JsonExtractSchema, JsonSetSchema, JsonRemoveSchema, } from "../types.js";
|
|
12
|
+
import { JsonValidOutputSchema, JsonExtractOutputSchema, JsonSetOutputSchema, JsonRemoveOutputSchema, JsonTypeOutputSchema, JsonArrayLengthOutputSchema, JsonKeysOutputSchema, JsonEachOutputSchema, JsonGroupArrayOutputSchema, JsonGroupObjectOutputSchema, JsonPrettyOutputSchema, JsonbConvertOutputSchema, JsonStorageInfoOutputSchema, JsonNormalizeColumnOutputSchema, } from "../output-schemas.js";
|
|
13
|
+
import { normalizeJson, isJsonbSupported, detectJsonStorageFormat, } from "../json-utils.js";
|
|
14
|
+
// Additional schemas for JSON operations
|
|
15
|
+
const JsonTypeSchema = z.object({
|
|
16
|
+
table: z.string().describe("Table name"),
|
|
17
|
+
column: z.string().describe("JSON column name"),
|
|
18
|
+
path: z.string().optional().describe("JSON path (defaults to $)"),
|
|
19
|
+
whereClause: z.string().optional(),
|
|
20
|
+
});
|
|
21
|
+
const JsonArrayLengthSchema = z.object({
|
|
22
|
+
table: z.string().describe("Table name"),
|
|
23
|
+
column: z.string().describe("JSON column name"),
|
|
24
|
+
path: z.string().optional().describe("Path to array (defaults to $)"),
|
|
25
|
+
whereClause: z.string().optional(),
|
|
26
|
+
});
|
|
27
|
+
const JsonArrayAppendSchema = z.object({
|
|
28
|
+
table: z.string().describe("Table name"),
|
|
29
|
+
column: z.string().describe("JSON column name"),
|
|
30
|
+
path: z.string().describe("Path to array"),
|
|
31
|
+
value: z.unknown().describe("Value to append"),
|
|
32
|
+
whereClause: z.string().describe("WHERE clause"),
|
|
33
|
+
});
|
|
34
|
+
const JsonKeysSchema = z.object({
|
|
35
|
+
table: z.string().describe("Table name"),
|
|
36
|
+
column: z.string().describe("JSON column name"),
|
|
37
|
+
path: z.string().optional().describe("Path to object (defaults to $)"),
|
|
38
|
+
whereClause: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
const JsonEachSchema = z.object({
|
|
41
|
+
table: z.string().describe("Table name"),
|
|
42
|
+
column: z.string().describe("JSON column name"),
|
|
43
|
+
path: z.string().optional().describe("Path to expand (defaults to $)"),
|
|
44
|
+
whereClause: z.string().optional(),
|
|
45
|
+
limit: z.number().optional().default(100),
|
|
46
|
+
});
|
|
47
|
+
const JsonGroupArraySchema = z.object({
|
|
48
|
+
table: z.string().describe("Table name"),
|
|
49
|
+
valueColumn: z
|
|
50
|
+
.string()
|
|
51
|
+
.describe("Column to aggregate (or SQL expression if allowExpressions is true)"),
|
|
52
|
+
groupByColumn: z
|
|
53
|
+
.string()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Column to group by. For JSON collection tables, use allowExpressions with json_extract(data, '$.field') instead."),
|
|
56
|
+
whereClause: z.string().optional(),
|
|
57
|
+
allowExpressions: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Allow SQL expressions like json_extract() instead of plain column names"),
|
|
61
|
+
});
|
|
62
|
+
const JsonGroupObjectSchema = z.object({
|
|
63
|
+
table: z.string().describe("Table name"),
|
|
64
|
+
keyColumn: z
|
|
65
|
+
.string()
|
|
66
|
+
.describe("Column for object keys (or SQL expression if allowExpressions is true)"),
|
|
67
|
+
valueColumn: z
|
|
68
|
+
.string()
|
|
69
|
+
.optional()
|
|
70
|
+
.describe("Column for object values (or SQL expression if allowExpressions is true). For aggregates like COUNT(*), use aggregateFunction instead."),
|
|
71
|
+
groupByColumn: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe("Column to group by. For JSON collection tables, use allowExpressions with json_extract(data, '$.field') instead."),
|
|
75
|
+
whereClause: z.string().optional(),
|
|
76
|
+
allowExpressions: z
|
|
77
|
+
.boolean()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Allow SQL expressions like json_extract() instead of plain column names. NOTE: Does NOT support aggregate functions - use aggregateFunction parameter instead."),
|
|
80
|
+
aggregateFunction: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe("Aggregate function to use for values (e.g., 'COUNT(*)', 'SUM(amount)', 'AVG(price)'). When provided, builds object from pre-aggregated subquery results."),
|
|
84
|
+
});
|
|
85
|
+
const JsonPrettySchema = z.object({
|
|
86
|
+
json: z.string().describe("JSON string to pretty print"),
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Get all JSON operation tools
|
|
90
|
+
*/
|
|
91
|
+
export function getJsonOperationTools(adapter) {
|
|
92
|
+
return [
|
|
93
|
+
createValidateJsonTool(),
|
|
94
|
+
createJsonExtractTool(adapter),
|
|
95
|
+
createJsonSetTool(adapter),
|
|
96
|
+
createJsonRemoveTool(adapter),
|
|
97
|
+
createJsonTypeTool(adapter),
|
|
98
|
+
createJsonArrayLengthTool(adapter),
|
|
99
|
+
createJsonArrayAppendTool(adapter),
|
|
100
|
+
createJsonKeysTool(adapter),
|
|
101
|
+
createJsonEachTool(adapter),
|
|
102
|
+
createJsonGroupArrayTool(adapter),
|
|
103
|
+
createJsonGroupObjectTool(adapter),
|
|
104
|
+
createJsonPrettyTool(),
|
|
105
|
+
// JSONB tools
|
|
106
|
+
createJsonbConvertTool(adapter),
|
|
107
|
+
createJsonStorageInfoTool(adapter),
|
|
108
|
+
createJsonNormalizeColumnTool(adapter),
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validate JSON string
|
|
113
|
+
*/
|
|
114
|
+
function createValidateJsonTool() {
|
|
115
|
+
return {
|
|
116
|
+
name: "sqlite_json_valid",
|
|
117
|
+
description: "Check if a string is valid JSON.",
|
|
118
|
+
group: "json",
|
|
119
|
+
inputSchema: ValidateJsonSchema,
|
|
120
|
+
outputSchema: JsonValidOutputSchema,
|
|
121
|
+
requiredScopes: ["read"],
|
|
122
|
+
annotations: readOnly("Validate JSON"),
|
|
123
|
+
handler: (params, _context) => {
|
|
124
|
+
const input = ValidateJsonSchema.parse(params);
|
|
125
|
+
try {
|
|
126
|
+
JSON.parse(input.json);
|
|
127
|
+
return Promise.resolve({
|
|
128
|
+
success: true,
|
|
129
|
+
valid: true,
|
|
130
|
+
message: "Valid JSON",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return Promise.resolve({
|
|
135
|
+
success: true,
|
|
136
|
+
valid: false,
|
|
137
|
+
message: error instanceof Error ? error.message : "Invalid JSON",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract value from JSON
|
|
145
|
+
*/
|
|
146
|
+
function createJsonExtractTool(adapter) {
|
|
147
|
+
return {
|
|
148
|
+
name: "sqlite_json_extract",
|
|
149
|
+
description: "Extract a value from a JSON column at the specified path using json_extract().",
|
|
150
|
+
group: "json",
|
|
151
|
+
inputSchema: JsonExtractSchema,
|
|
152
|
+
outputSchema: JsonExtractOutputSchema,
|
|
153
|
+
requiredScopes: ["read"],
|
|
154
|
+
annotations: readOnly("JSON Extract"),
|
|
155
|
+
handler: async (params, _context) => {
|
|
156
|
+
const input = JsonExtractSchema.parse(params);
|
|
157
|
+
// Validate and quote identifiers
|
|
158
|
+
const table = sanitizeIdentifier(input.table);
|
|
159
|
+
const column = sanitizeIdentifier(input.column);
|
|
160
|
+
if (!input.path.startsWith("$")) {
|
|
161
|
+
throw new Error("JSON path must start with $");
|
|
162
|
+
}
|
|
163
|
+
let sql = `SELECT json_extract(${column}, '${input.path}') as value FROM ${table}`;
|
|
164
|
+
if (input.whereClause) {
|
|
165
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
166
|
+
}
|
|
167
|
+
const result = await adapter.executeReadQuery(sql);
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
rowCount: result.rows?.length ?? 0,
|
|
171
|
+
values: result.rows?.map((r) => r["value"]),
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Set value in JSON
|
|
178
|
+
*/
|
|
179
|
+
function createJsonSetTool(adapter) {
|
|
180
|
+
return {
|
|
181
|
+
name: "sqlite_json_set",
|
|
182
|
+
description: "Set a value at a JSON path using json_set(). Creates path if it does not exist.",
|
|
183
|
+
group: "json",
|
|
184
|
+
inputSchema: JsonSetSchema,
|
|
185
|
+
outputSchema: JsonSetOutputSchema,
|
|
186
|
+
requiredScopes: ["write"],
|
|
187
|
+
annotations: write("JSON Set"),
|
|
188
|
+
handler: async (params, _context) => {
|
|
189
|
+
const input = JsonSetSchema.parse(params);
|
|
190
|
+
// Validate and quote identifiers
|
|
191
|
+
const table = sanitizeIdentifier(input.table);
|
|
192
|
+
const column = sanitizeIdentifier(input.column);
|
|
193
|
+
if (!input.path.startsWith("$")) {
|
|
194
|
+
throw new Error("JSON path must start with $");
|
|
195
|
+
}
|
|
196
|
+
const valueJson = JSON.stringify(input.value);
|
|
197
|
+
const sql = `UPDATE ${table} SET ${column} = json_set(${column}, '${input.path}', json('${valueJson.replace(/'/g, "''")}')) WHERE ${input.whereClause}`;
|
|
198
|
+
const result = await adapter.executeWriteQuery(sql);
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
rowsAffected: result.rowsAffected,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Remove value from JSON
|
|
208
|
+
*/
|
|
209
|
+
function createJsonRemoveTool(adapter) {
|
|
210
|
+
return {
|
|
211
|
+
name: "sqlite_json_remove",
|
|
212
|
+
description: "Remove a value at a JSON path using json_remove().",
|
|
213
|
+
group: "json",
|
|
214
|
+
inputSchema: JsonRemoveSchema,
|
|
215
|
+
outputSchema: JsonRemoveOutputSchema,
|
|
216
|
+
requiredScopes: ["write"],
|
|
217
|
+
annotations: write("JSON Remove"),
|
|
218
|
+
handler: async (params, _context) => {
|
|
219
|
+
const input = JsonRemoveSchema.parse(params);
|
|
220
|
+
// Validate and quote identifiers
|
|
221
|
+
const table = sanitizeIdentifier(input.table);
|
|
222
|
+
const column = sanitizeIdentifier(input.column);
|
|
223
|
+
if (!input.path.startsWith("$")) {
|
|
224
|
+
throw new Error("JSON path must start with $");
|
|
225
|
+
}
|
|
226
|
+
const sql = `UPDATE ${table} SET ${column} = json_remove(${column}, '${input.path}') WHERE ${input.whereClause}`;
|
|
227
|
+
const result = await adapter.executeWriteQuery(sql);
|
|
228
|
+
return {
|
|
229
|
+
success: true,
|
|
230
|
+
rowsAffected: result.rowsAffected,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get JSON value type
|
|
237
|
+
*/
|
|
238
|
+
function createJsonTypeTool(adapter) {
|
|
239
|
+
return {
|
|
240
|
+
name: "sqlite_json_type",
|
|
241
|
+
description: "Get the JSON type (null, true, false, integer, real, text, array, object) at a path.",
|
|
242
|
+
group: "json",
|
|
243
|
+
inputSchema: JsonTypeSchema,
|
|
244
|
+
outputSchema: JsonTypeOutputSchema,
|
|
245
|
+
requiredScopes: ["read"],
|
|
246
|
+
annotations: readOnly("JSON Type"),
|
|
247
|
+
handler: async (params, _context) => {
|
|
248
|
+
const input = JsonTypeSchema.parse(params);
|
|
249
|
+
// Validate and quote identifiers
|
|
250
|
+
const table = sanitizeIdentifier(input.table);
|
|
251
|
+
const column = sanitizeIdentifier(input.column);
|
|
252
|
+
const path = input.path ?? "$";
|
|
253
|
+
if (!path.startsWith("$")) {
|
|
254
|
+
throw new Error("JSON path must start with $");
|
|
255
|
+
}
|
|
256
|
+
let sql = `SELECT json_type(${column}, '${path}') as type FROM ${table}`;
|
|
257
|
+
if (input.whereClause) {
|
|
258
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
259
|
+
}
|
|
260
|
+
const result = await adapter.executeReadQuery(sql);
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
rowCount: result.rows?.length ?? 0,
|
|
264
|
+
types: result.rows?.map((r) => r["type"]),
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get JSON array length
|
|
271
|
+
*/
|
|
272
|
+
function createJsonArrayLengthTool(adapter) {
|
|
273
|
+
return {
|
|
274
|
+
name: "sqlite_json_array_length",
|
|
275
|
+
description: "Get the length of a JSON array at the specified path.",
|
|
276
|
+
group: "json",
|
|
277
|
+
inputSchema: JsonArrayLengthSchema,
|
|
278
|
+
outputSchema: JsonArrayLengthOutputSchema,
|
|
279
|
+
requiredScopes: ["read"],
|
|
280
|
+
annotations: readOnly("Array Length"),
|
|
281
|
+
handler: async (params, _context) => {
|
|
282
|
+
const input = JsonArrayLengthSchema.parse(params);
|
|
283
|
+
// Validate and quote identifiers
|
|
284
|
+
const table = sanitizeIdentifier(input.table);
|
|
285
|
+
const column = sanitizeIdentifier(input.column);
|
|
286
|
+
const path = input.path ?? "$";
|
|
287
|
+
if (!path.startsWith("$")) {
|
|
288
|
+
throw new Error("JSON path must start with $");
|
|
289
|
+
}
|
|
290
|
+
let sql = `SELECT json_array_length(${column}, '${path}') as length FROM ${table}`;
|
|
291
|
+
if (input.whereClause) {
|
|
292
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
293
|
+
}
|
|
294
|
+
const result = await adapter.executeReadQuery(sql);
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
rowCount: result.rows?.length ?? 0,
|
|
298
|
+
lengths: result.rows?.map((r) => r["length"]),
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Append to JSON array
|
|
305
|
+
*/
|
|
306
|
+
function createJsonArrayAppendTool(adapter) {
|
|
307
|
+
return {
|
|
308
|
+
name: "sqlite_json_array_append",
|
|
309
|
+
description: "Append a value to a JSON array using json_insert().",
|
|
310
|
+
group: "json",
|
|
311
|
+
inputSchema: JsonArrayAppendSchema,
|
|
312
|
+
outputSchema: JsonSetOutputSchema,
|
|
313
|
+
requiredScopes: ["write"],
|
|
314
|
+
annotations: write("Array Append"),
|
|
315
|
+
handler: async (params, _context) => {
|
|
316
|
+
const input = JsonArrayAppendSchema.parse(params);
|
|
317
|
+
// Validate and quote identifiers
|
|
318
|
+
const table = sanitizeIdentifier(input.table);
|
|
319
|
+
const column = sanitizeIdentifier(input.column);
|
|
320
|
+
if (!input.path.startsWith("$")) {
|
|
321
|
+
throw new Error("JSON path must start with $");
|
|
322
|
+
}
|
|
323
|
+
const valueJson = JSON.stringify(input.value);
|
|
324
|
+
// Append by using [#] which means "end of array"
|
|
325
|
+
const appendPath = input.path.endsWith("]")
|
|
326
|
+
? input.path.replace(/\]$/, "#]")
|
|
327
|
+
: `${input.path}[#]`;
|
|
328
|
+
const sql = `UPDATE ${table} SET ${column} = json_insert(${column}, '${appendPath}', json('${valueJson.replace(/'/g, "''")}')) WHERE ${input.whereClause}`;
|
|
329
|
+
const result = await adapter.executeWriteQuery(sql);
|
|
330
|
+
return {
|
|
331
|
+
success: true,
|
|
332
|
+
rowsAffected: result.rowsAffected,
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get JSON object keys
|
|
339
|
+
*/
|
|
340
|
+
function createJsonKeysTool(adapter) {
|
|
341
|
+
return {
|
|
342
|
+
name: "sqlite_json_keys",
|
|
343
|
+
description: "Get the distinct keys of JSON objects at the specified path (returns unique keys across all matching rows).",
|
|
344
|
+
group: "json",
|
|
345
|
+
inputSchema: JsonKeysSchema,
|
|
346
|
+
outputSchema: JsonKeysOutputSchema,
|
|
347
|
+
requiredScopes: ["read"],
|
|
348
|
+
annotations: readOnly("JSON Keys"),
|
|
349
|
+
handler: async (params, _context) => {
|
|
350
|
+
const input = JsonKeysSchema.parse(params);
|
|
351
|
+
// Validate and quote identifiers
|
|
352
|
+
const table = sanitizeIdentifier(input.table);
|
|
353
|
+
const column = sanitizeIdentifier(input.column);
|
|
354
|
+
const path = input.path ?? "$";
|
|
355
|
+
if (!path.startsWith("$")) {
|
|
356
|
+
throw new Error("JSON path must start with $");
|
|
357
|
+
}
|
|
358
|
+
// Use subquery to avoid ambiguous column when table has a 'key' or 'id' column
|
|
359
|
+
// json_each returns: key, value, type, atom, id, parent, fullkey, path
|
|
360
|
+
let sql;
|
|
361
|
+
if (input.whereClause) {
|
|
362
|
+
// With WHERE clause, use subquery to isolate table columns from json_each columns
|
|
363
|
+
// This avoids ambiguity between e.g. table.id and json_each.id
|
|
364
|
+
sql = `SELECT DISTINCT json_each.key
|
|
365
|
+
FROM json_each(
|
|
366
|
+
(SELECT ${column} FROM ${table} WHERE ${input.whereClause} LIMIT 1),
|
|
367
|
+
'${path}'
|
|
368
|
+
)`;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// Without WHERE, simpler subquery avoids 'key' column ambiguity
|
|
372
|
+
sql = `SELECT DISTINCT json_each.key FROM ${table} AS t, json_each(t.${column}, '${path}')`;
|
|
373
|
+
}
|
|
374
|
+
const result = await adapter.executeReadQuery(sql);
|
|
375
|
+
const keys = result.rows?.map((r) => r["key"]) ?? [];
|
|
376
|
+
return {
|
|
377
|
+
success: true,
|
|
378
|
+
rowCount: keys.length,
|
|
379
|
+
keys: keys,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Expand JSON to rows
|
|
386
|
+
*/
|
|
387
|
+
function createJsonEachTool(adapter) {
|
|
388
|
+
return {
|
|
389
|
+
name: "sqlite_json_each",
|
|
390
|
+
description: "Expand a JSON array or object into rows using json_each().",
|
|
391
|
+
group: "json",
|
|
392
|
+
inputSchema: JsonEachSchema,
|
|
393
|
+
outputSchema: JsonEachOutputSchema,
|
|
394
|
+
requiredScopes: ["read"],
|
|
395
|
+
annotations: readOnly("JSON Each"),
|
|
396
|
+
handler: async (params, _context) => {
|
|
397
|
+
const input = JsonEachSchema.parse(params);
|
|
398
|
+
// Validate and quote identifiers
|
|
399
|
+
const table = sanitizeIdentifier(input.table);
|
|
400
|
+
const column = sanitizeIdentifier(input.column);
|
|
401
|
+
const path = input.path ?? "$";
|
|
402
|
+
if (!path.startsWith("$")) {
|
|
403
|
+
throw new Error("JSON path must start with $");
|
|
404
|
+
}
|
|
405
|
+
// Use table alias and CROSS JOIN to avoid ambiguity with json_each() output columns
|
|
406
|
+
// json_each() returns: key, value, type, atom, id, parent, fullkey, path
|
|
407
|
+
// If the source table has any of these columns (e.g., 'id'), they must be qualified
|
|
408
|
+
let sql = `SELECT t.rowid as row_id, je.key, je.value, je.type FROM ${table} AS t CROSS JOIN json_each(t.${column}, '${path}') AS je`;
|
|
409
|
+
if (input.whereClause) {
|
|
410
|
+
// Qualify unqualified 'id' column references with table alias 't.'
|
|
411
|
+
// This handles: id = X, id IN (...), id BETWEEN, id IS NULL, etc.
|
|
412
|
+
// Won't match already-qualified refs like 't.id' or 'je.id'
|
|
413
|
+
const qualifiedWhere = input.whereClause.replace(/(?<![.\w])id(?=\s*[=<>!]|\s+(?:IN|BETWEEN|IS|LIKE)\b)/gi, "t.id");
|
|
414
|
+
sql += ` WHERE ${qualifiedWhere}`;
|
|
415
|
+
}
|
|
416
|
+
sql += ` LIMIT ${input.limit}`;
|
|
417
|
+
const result = await adapter.executeReadQuery(sql);
|
|
418
|
+
return {
|
|
419
|
+
success: true,
|
|
420
|
+
rowCount: result.rows?.length ?? 0,
|
|
421
|
+
elements: result.rows,
|
|
422
|
+
};
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Aggregate values into JSON array
|
|
428
|
+
*/
|
|
429
|
+
function createJsonGroupArrayTool(adapter) {
|
|
430
|
+
return {
|
|
431
|
+
name: "sqlite_json_group_array",
|
|
432
|
+
description: "Aggregate column values into a JSON array using json_group_array().",
|
|
433
|
+
group: "json",
|
|
434
|
+
inputSchema: JsonGroupArraySchema,
|
|
435
|
+
outputSchema: JsonGroupArrayOutputSchema,
|
|
436
|
+
requiredScopes: ["read"],
|
|
437
|
+
annotations: readOnly("Group Array"),
|
|
438
|
+
handler: async (params, _context) => {
|
|
439
|
+
const input = JsonGroupArraySchema.parse(params);
|
|
440
|
+
// Validate table name (always required)
|
|
441
|
+
const table = sanitizeIdentifier(input.table);
|
|
442
|
+
// Allow raw SQL expressions when allowExpressions is true
|
|
443
|
+
// This enables use cases like: json_extract(data, '$.name')
|
|
444
|
+
let valueColumn;
|
|
445
|
+
if (input.allowExpressions) {
|
|
446
|
+
// Use expression directly (user takes responsibility for SQL safety)
|
|
447
|
+
valueColumn = input.valueColumn;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Validate as identifier (default, safe behavior)
|
|
451
|
+
valueColumn = sanitizeIdentifier(input.valueColumn);
|
|
452
|
+
}
|
|
453
|
+
let selectClause = `json_group_array(${valueColumn}) as array_result`;
|
|
454
|
+
let groupByClause = "";
|
|
455
|
+
if (input.groupByColumn) {
|
|
456
|
+
// Apply allowExpressions to groupByColumn as well
|
|
457
|
+
const groupByCol = input.allowExpressions
|
|
458
|
+
? input.groupByColumn
|
|
459
|
+
: sanitizeIdentifier(input.groupByColumn);
|
|
460
|
+
// Use alias for clean output; for expressions use 'group_key' alias
|
|
461
|
+
const groupAlias = input.allowExpressions
|
|
462
|
+
? "group_key"
|
|
463
|
+
: input.groupByColumn;
|
|
464
|
+
selectClause = `${groupByCol} AS ${groupAlias}, ${selectClause}`;
|
|
465
|
+
groupByClause = ` GROUP BY ${groupByCol}`;
|
|
466
|
+
}
|
|
467
|
+
let sql = `SELECT ${selectClause} FROM ${table}`;
|
|
468
|
+
if (input.whereClause) {
|
|
469
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
470
|
+
}
|
|
471
|
+
sql += groupByClause;
|
|
472
|
+
const result = await adapter.executeReadQuery(sql);
|
|
473
|
+
return {
|
|
474
|
+
success: true,
|
|
475
|
+
rowCount: result.rows?.length ?? 0,
|
|
476
|
+
rows: result.rows ?? [],
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Aggregate key-value pairs into JSON object
|
|
483
|
+
*/
|
|
484
|
+
function createJsonGroupObjectTool(adapter) {
|
|
485
|
+
return {
|
|
486
|
+
name: "sqlite_json_group_object",
|
|
487
|
+
description: "Aggregate key-value pairs into a JSON object using json_group_object().",
|
|
488
|
+
group: "json",
|
|
489
|
+
inputSchema: JsonGroupObjectSchema,
|
|
490
|
+
outputSchema: JsonGroupObjectOutputSchema,
|
|
491
|
+
requiredScopes: ["read"],
|
|
492
|
+
annotations: readOnly("Group Object"),
|
|
493
|
+
handler: async (params, _context) => {
|
|
494
|
+
const input = JsonGroupObjectSchema.parse(params);
|
|
495
|
+
// Validate table name (always required)
|
|
496
|
+
const table = sanitizeIdentifier(input.table);
|
|
497
|
+
// Handle aggregate function mode - uses subquery pattern
|
|
498
|
+
// This enables COUNT(*), SUM(x), AVG(x), etc. as values
|
|
499
|
+
if (input.aggregateFunction) {
|
|
500
|
+
// Build the key column expression
|
|
501
|
+
const keyCol = input.allowExpressions
|
|
502
|
+
? input.keyColumn
|
|
503
|
+
: sanitizeIdentifier(input.keyColumn);
|
|
504
|
+
// Build subquery that computes the aggregate grouped by key
|
|
505
|
+
let subquery = `SELECT ${keyCol} as agg_key, ${input.aggregateFunction} as agg_value FROM ${table}`;
|
|
506
|
+
if (input.whereClause) {
|
|
507
|
+
subquery += ` WHERE ${input.whereClause}`;
|
|
508
|
+
}
|
|
509
|
+
subquery += ` GROUP BY ${keyCol}`;
|
|
510
|
+
// Outer query wraps the aggregates into a JSON object
|
|
511
|
+
const outerSelect = `json_group_object(agg_key, agg_value) as object_result`;
|
|
512
|
+
const outerGroupBy = "";
|
|
513
|
+
if (input.groupByColumn) {
|
|
514
|
+
// For nested grouping, we need a more complex approach with window functions or correlated subqueries
|
|
515
|
+
// For now, outer grouping with aggregates is not supported - return error with guidance
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
error: "groupByColumn is not supported when using aggregateFunction. Use a separate query for each group.",
|
|
519
|
+
rowCount: 0,
|
|
520
|
+
rows: [],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const sql = `SELECT ${outerSelect} FROM (${subquery})${outerGroupBy}`;
|
|
524
|
+
const result = await adapter.executeReadQuery(sql);
|
|
525
|
+
return {
|
|
526
|
+
success: true,
|
|
527
|
+
rowCount: result.rows?.length ?? 0,
|
|
528
|
+
rows: result.rows ?? [],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// Standard mode: valueColumn is required when not using aggregateFunction
|
|
532
|
+
if (!input.valueColumn) {
|
|
533
|
+
return {
|
|
534
|
+
success: false,
|
|
535
|
+
error: "valueColumn is required unless using aggregateFunction parameter",
|
|
536
|
+
rowCount: 0,
|
|
537
|
+
rows: [],
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
// Warn when allowExpressions is used without groupByColumn - can produce duplicate keys
|
|
541
|
+
// Each row creates a key-value pair; if multiple rows have the same key, duplicates result
|
|
542
|
+
const duplicateKeyWarning = input.allowExpressions && !input.groupByColumn
|
|
543
|
+
? "Warning: Using allowExpressions without groupByColumn may produce duplicate keys if key values aren't unique. Consider using groupByColumn, aggregateFunction, or ensuring key uniqueness."
|
|
544
|
+
: undefined;
|
|
545
|
+
// Allow raw SQL expressions when allowExpressions is true
|
|
546
|
+
// This enables use cases like: json_extract(data, '$.name')
|
|
547
|
+
let keyColumn;
|
|
548
|
+
let valueColumn;
|
|
549
|
+
if (input.allowExpressions) {
|
|
550
|
+
// Use expressions directly (user takes responsibility for SQL safety)
|
|
551
|
+
keyColumn = input.keyColumn;
|
|
552
|
+
valueColumn = input.valueColumn;
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
// Validate as identifiers (default, safe behavior)
|
|
556
|
+
keyColumn = sanitizeIdentifier(input.keyColumn);
|
|
557
|
+
valueColumn = sanitizeIdentifier(input.valueColumn);
|
|
558
|
+
}
|
|
559
|
+
let selectClause = `json_group_object(${keyColumn}, ${valueColumn}) as object_result`;
|
|
560
|
+
let groupByClause = "";
|
|
561
|
+
if (input.groupByColumn) {
|
|
562
|
+
// Apply allowExpressions to groupByColumn as well
|
|
563
|
+
const groupByCol = input.allowExpressions
|
|
564
|
+
? input.groupByColumn
|
|
565
|
+
: sanitizeIdentifier(input.groupByColumn);
|
|
566
|
+
// Use alias for clean output; for expressions use 'group_key' alias
|
|
567
|
+
const groupAlias = input.allowExpressions
|
|
568
|
+
? "group_key"
|
|
569
|
+
: input.groupByColumn;
|
|
570
|
+
selectClause = `${groupByCol} AS ${groupAlias}, ${selectClause}`;
|
|
571
|
+
groupByClause = ` GROUP BY ${groupByCol}`;
|
|
572
|
+
}
|
|
573
|
+
let sql = `SELECT ${selectClause} FROM ${table}`;
|
|
574
|
+
if (input.whereClause) {
|
|
575
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
576
|
+
}
|
|
577
|
+
sql += groupByClause;
|
|
578
|
+
const result = await adapter.executeReadQuery(sql);
|
|
579
|
+
return {
|
|
580
|
+
success: true,
|
|
581
|
+
rowCount: result.rows?.length ?? 0,
|
|
582
|
+
rows: result.rows ?? [],
|
|
583
|
+
...(duplicateKeyWarning && { hint: duplicateKeyWarning }),
|
|
584
|
+
};
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Pretty print and compact JSON
|
|
590
|
+
*/
|
|
591
|
+
function createJsonPrettyTool() {
|
|
592
|
+
return {
|
|
593
|
+
name: "sqlite_json_pretty",
|
|
594
|
+
description: "Format JSON string with indentation for readability.",
|
|
595
|
+
group: "json",
|
|
596
|
+
inputSchema: JsonPrettySchema,
|
|
597
|
+
outputSchema: JsonPrettyOutputSchema,
|
|
598
|
+
requiredScopes: ["read"],
|
|
599
|
+
annotations: readOnly("JSON Pretty"),
|
|
600
|
+
handler: (params, _context) => {
|
|
601
|
+
const input = JsonPrettySchema.parse(params);
|
|
602
|
+
try {
|
|
603
|
+
const parsed = JSON.parse(input.json);
|
|
604
|
+
const pretty = JSON.stringify(parsed, null, 2);
|
|
605
|
+
return Promise.resolve({
|
|
606
|
+
success: true,
|
|
607
|
+
formatted: pretty,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
return Promise.resolve({
|
|
612
|
+
success: false,
|
|
613
|
+
error: error instanceof Error ? error.message : "Invalid JSON",
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
// =============================================================================
|
|
620
|
+
// JSONB Tools
|
|
621
|
+
// =============================================================================
|
|
622
|
+
// Schema for JSONB convert tool
|
|
623
|
+
const JsonbConvertSchema = z.object({
|
|
624
|
+
table: z.string().describe("Table name"),
|
|
625
|
+
column: z.string().describe("JSON column to convert"),
|
|
626
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
627
|
+
});
|
|
628
|
+
// Schema for storage info tool
|
|
629
|
+
const JsonStorageInfoSchema = z.object({
|
|
630
|
+
table: z.string().describe("Table name"),
|
|
631
|
+
column: z.string().describe("JSON column to analyze"),
|
|
632
|
+
sampleSize: z
|
|
633
|
+
.number()
|
|
634
|
+
.optional()
|
|
635
|
+
.default(100)
|
|
636
|
+
.describe("Number of rows to sample"),
|
|
637
|
+
});
|
|
638
|
+
// Schema for normalize column tool
|
|
639
|
+
const JsonNormalizeColumnSchema = z.object({
|
|
640
|
+
table: z.string().describe("Table name"),
|
|
641
|
+
column: z.string().describe("JSON column to normalize"),
|
|
642
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
643
|
+
outputFormat: z
|
|
644
|
+
.enum(["text", "jsonb", "preserve"])
|
|
645
|
+
.optional()
|
|
646
|
+
.default("preserve")
|
|
647
|
+
.describe("Output format: 'preserve' original format (default), 'text', or 'jsonb'"),
|
|
648
|
+
});
|
|
649
|
+
/**
|
|
650
|
+
* Convert text JSON column to JSONB format
|
|
651
|
+
*/
|
|
652
|
+
function createJsonbConvertTool(adapter) {
|
|
653
|
+
return {
|
|
654
|
+
name: "sqlite_jsonb_convert",
|
|
655
|
+
description: "Convert a text JSON column to JSONB binary format for faster processing. Requires SQLite 3.45+.",
|
|
656
|
+
group: "json",
|
|
657
|
+
inputSchema: JsonbConvertSchema,
|
|
658
|
+
outputSchema: JsonbConvertOutputSchema,
|
|
659
|
+
requiredScopes: ["write"],
|
|
660
|
+
annotations: write("JSONB Convert"),
|
|
661
|
+
handler: async (params, _context) => {
|
|
662
|
+
const input = JsonbConvertSchema.parse(params);
|
|
663
|
+
// Validate and quote identifiers
|
|
664
|
+
const table = sanitizeIdentifier(input.table);
|
|
665
|
+
const column = sanitizeIdentifier(input.column);
|
|
666
|
+
// Check JSONB support
|
|
667
|
+
if (!isJsonbSupported()) {
|
|
668
|
+
return {
|
|
669
|
+
success: false,
|
|
670
|
+
error: "JSONB not supported (requires SQLite 3.45+)",
|
|
671
|
+
hint: "Current SQLite version does not support JSONB. Data remains as text JSON.",
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
let sql = `UPDATE ${table} SET ${column} = jsonb(${column})`;
|
|
675
|
+
if (input.whereClause) {
|
|
676
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
677
|
+
}
|
|
678
|
+
const result = await adapter.executeWriteQuery(sql);
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
message: `Converted ${result.rowsAffected} rows to JSONB format`,
|
|
682
|
+
rowsAffected: result.rowsAffected,
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Get storage format info for a JSON column
|
|
689
|
+
*/
|
|
690
|
+
function createJsonStorageInfoTool(adapter) {
|
|
691
|
+
return {
|
|
692
|
+
name: "sqlite_json_storage_info",
|
|
693
|
+
description: "Analyze storage format of a JSON column (text vs JSONB) and report statistics.",
|
|
694
|
+
group: "json",
|
|
695
|
+
inputSchema: JsonStorageInfoSchema,
|
|
696
|
+
outputSchema: JsonStorageInfoOutputSchema,
|
|
697
|
+
requiredScopes: ["read"],
|
|
698
|
+
annotations: readOnly("JSON Storage Info"),
|
|
699
|
+
handler: async (params, _context) => {
|
|
700
|
+
const input = JsonStorageInfoSchema.parse(params);
|
|
701
|
+
// Validate identifiers
|
|
702
|
+
const table = sanitizeIdentifier(input.table);
|
|
703
|
+
// Sample rows to detect format
|
|
704
|
+
const sql = `SELECT ${sanitizeIdentifier(input.column)} FROM ${table} LIMIT ${input.sampleSize}`;
|
|
705
|
+
const result = await adapter.executeReadQuery(sql);
|
|
706
|
+
let textCount = 0;
|
|
707
|
+
let jsonbCount = 0;
|
|
708
|
+
let nullCount = 0;
|
|
709
|
+
let unknownCount = 0;
|
|
710
|
+
for (const row of result.rows ?? []) {
|
|
711
|
+
const value = row[input.column];
|
|
712
|
+
if (value === null || value === undefined) {
|
|
713
|
+
nullCount++;
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
const format = detectJsonStorageFormat(value);
|
|
717
|
+
if (format === "text")
|
|
718
|
+
textCount++;
|
|
719
|
+
else if (format === "jsonb")
|
|
720
|
+
jsonbCount++;
|
|
721
|
+
else
|
|
722
|
+
unknownCount++;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const total = result.rows?.length ?? 0;
|
|
726
|
+
return {
|
|
727
|
+
success: true,
|
|
728
|
+
jsonbSupported: isJsonbSupported(),
|
|
729
|
+
sampleSize: total,
|
|
730
|
+
formats: {
|
|
731
|
+
text: textCount,
|
|
732
|
+
jsonb: jsonbCount,
|
|
733
|
+
null: nullCount,
|
|
734
|
+
unknown: unknownCount,
|
|
735
|
+
},
|
|
736
|
+
recommendation:
|
|
737
|
+
// Mixed format: both text and JSONB rows exist
|
|
738
|
+
textCount > 0 && jsonbCount > 0
|
|
739
|
+
? `Column has mixed formats (${textCount} text, ${jsonbCount} JSONB). Run sqlite_jsonb_convert to unify.`
|
|
740
|
+
: // All text, JSONB supported: recommend conversion
|
|
741
|
+
jsonbCount === 0 && textCount > 0 && isJsonbSupported()
|
|
742
|
+
? "Column uses text JSON. Consider converting to JSONB for better performance."
|
|
743
|
+
: // All JSONB: already optimal
|
|
744
|
+
jsonbCount > 0
|
|
745
|
+
? "Column already uses JSONB format."
|
|
746
|
+
: // No JSON data found
|
|
747
|
+
"No JSON data found in sample.",
|
|
748
|
+
};
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Normalize JSON data in a column for consistent storage
|
|
754
|
+
*
|
|
755
|
+
* Handles both text JSON and JSONB binary format by using SQL's json()
|
|
756
|
+
* function to read the data as text before JavaScript processing.
|
|
757
|
+
*/
|
|
758
|
+
function createJsonNormalizeColumnTool(adapter) {
|
|
759
|
+
return {
|
|
760
|
+
name: "sqlite_json_normalize_column",
|
|
761
|
+
description: "Normalize JSON data in a column (sort keys, compact format) for consistent storage and comparison.",
|
|
762
|
+
group: "json",
|
|
763
|
+
inputSchema: JsonNormalizeColumnSchema,
|
|
764
|
+
outputSchema: JsonNormalizeColumnOutputSchema,
|
|
765
|
+
requiredScopes: ["write"],
|
|
766
|
+
annotations: write("Normalize JSON Column"),
|
|
767
|
+
handler: async (params, _context) => {
|
|
768
|
+
const input = JsonNormalizeColumnSchema.parse(params);
|
|
769
|
+
// Validate and quote identifiers
|
|
770
|
+
const table = sanitizeIdentifier(input.table);
|
|
771
|
+
const column = sanitizeIdentifier(input.column);
|
|
772
|
+
// Select both the raw column value (to detect JSONB format) and the text
|
|
773
|
+
// representation via json(). This allows us to:
|
|
774
|
+
// 1. Detect if original storage is JSONB (binary blob)
|
|
775
|
+
// 2. Get text JSON for normalization processing
|
|
776
|
+
let selectSql = `SELECT rowid, ${column} as raw_data, json(${column}) as json_data FROM ${table}`;
|
|
777
|
+
if (input.whereClause) {
|
|
778
|
+
selectSql += ` WHERE ${input.whereClause}`;
|
|
779
|
+
}
|
|
780
|
+
const selectResult = await adapter.executeReadQuery(selectSql);
|
|
781
|
+
let normalizedCount = 0;
|
|
782
|
+
let unchangedCount = 0;
|
|
783
|
+
let errorCount = 0;
|
|
784
|
+
// Normalize each row
|
|
785
|
+
for (const row of selectResult.rows ?? []) {
|
|
786
|
+
const rowid = row["rowid"];
|
|
787
|
+
const rawData = row["raw_data"];
|
|
788
|
+
const jsonData = row["json_data"];
|
|
789
|
+
if (jsonData === null || jsonData === undefined) {
|
|
790
|
+
unchangedCount++;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
const { normalized, wasModified } = normalizeJson(jsonData);
|
|
795
|
+
// Detect if original was stored as JSONB (binary blob)
|
|
796
|
+
// If rawData is not a string, it's likely JSONB binary data
|
|
797
|
+
// better-sqlite3 returns JSONB blobs as Buffer objects
|
|
798
|
+
const wasJsonb = rawData !== null &&
|
|
799
|
+
rawData !== undefined &&
|
|
800
|
+
typeof rawData !== "string";
|
|
801
|
+
// Determine target format based on outputFormat parameter
|
|
802
|
+
const targetFormat = input.outputFormat ?? "preserve";
|
|
803
|
+
const shouldOutputJsonb = targetFormat === "jsonb" ||
|
|
804
|
+
(targetFormat === "preserve" && wasJsonb);
|
|
805
|
+
// Determine if update is needed:
|
|
806
|
+
// - Content was modified (keys reordered, normalized)
|
|
807
|
+
// - Converting from JSONB to text (when outputFormat is 'text')
|
|
808
|
+
// - Converting from text to JSONB (when outputFormat is 'jsonb')
|
|
809
|
+
const needsFormatChange = (wasJsonb && targetFormat === "text") ||
|
|
810
|
+
(!wasJsonb && targetFormat === "jsonb");
|
|
811
|
+
if (wasModified || needsFormatChange) {
|
|
812
|
+
// Use jsonb() wrapper if target is JSONB, otherwise plain text
|
|
813
|
+
const updateSql = shouldOutputJsonb
|
|
814
|
+
? `UPDATE ${table} SET ${column} = jsonb(?) WHERE rowid = ?`
|
|
815
|
+
: `UPDATE ${table} SET ${column} = ? WHERE rowid = ?`;
|
|
816
|
+
await adapter.executeWriteQuery(updateSql, [normalized, rowid]);
|
|
817
|
+
normalizedCount++;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
unchangedCount++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
errorCount++;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
success: true,
|
|
829
|
+
message: `Normalized ${normalizedCount} rows`,
|
|
830
|
+
normalized: normalizedCount,
|
|
831
|
+
unchanged: unchangedCount,
|
|
832
|
+
errors: errorCount,
|
|
833
|
+
total: selectResult.rows?.length ?? 0,
|
|
834
|
+
outputFormat: input.outputFormat ?? "preserve",
|
|
835
|
+
};
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=json-operations.js.map
|