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,930 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Virtual Table Tools
|
|
3
|
+
*
|
|
4
|
+
* Create and manage virtual tables for CSV, R-Tree, generation, etc.
|
|
5
|
+
* 13 tools total.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { readOnly, idempotent, destructive, admin, } from "../../../utils/annotations.js";
|
|
10
|
+
import { buildProgressContext, sendProgress, } from "../../../utils/progress-utils.js";
|
|
11
|
+
import { sanitizeIdentifier } from "../../../utils/index.js";
|
|
12
|
+
import { GenerateSeriesOutputSchema, CreateTableOutputSchema, ListViewsOutputSchema, DropTableOutputSchema, VacuumOutputSchema, } from "../output-schemas.js";
|
|
13
|
+
import { isSpatialiteSystemView, isSpatialiteSystemTable, isSpatialiteSystemIndex, } from "./core.js";
|
|
14
|
+
// Virtual table schemas
|
|
15
|
+
const GenerateSeriesSchema = z.object({
|
|
16
|
+
start: z.number().describe("Start value"),
|
|
17
|
+
stop: z.number().describe("Stop value"),
|
|
18
|
+
step: z.number().optional().default(1).describe("Step value"),
|
|
19
|
+
});
|
|
20
|
+
const CreateViewSchema = z.object({
|
|
21
|
+
viewName: z.string().describe("Name of the view"),
|
|
22
|
+
selectQuery: z.string().describe("SELECT query for view definition"),
|
|
23
|
+
replace: z.boolean().optional().default(false),
|
|
24
|
+
});
|
|
25
|
+
const ListViewsSchema = z.object({
|
|
26
|
+
pattern: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Optional LIKE pattern to filter views"),
|
|
30
|
+
excludeSystemViews: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.default(true)
|
|
34
|
+
.describe("Exclude SpatiaLite system views (default: true). Set to false to include all views."),
|
|
35
|
+
});
|
|
36
|
+
const DropViewSchema = z.object({
|
|
37
|
+
viewName: z.string().describe("Name of the view to drop"),
|
|
38
|
+
ifExists: z.boolean().optional().default(true),
|
|
39
|
+
});
|
|
40
|
+
const DbStatSchema = z.object({
|
|
41
|
+
table: z.string().optional().describe("Optional table name to filter"),
|
|
42
|
+
summarize: z
|
|
43
|
+
.boolean()
|
|
44
|
+
.optional()
|
|
45
|
+
.default(false)
|
|
46
|
+
.describe("If true, return aggregated per-table stats instead of raw page-level data"),
|
|
47
|
+
limit: z
|
|
48
|
+
.number()
|
|
49
|
+
.optional()
|
|
50
|
+
.default(100)
|
|
51
|
+
.describe("Maximum number of tables/pages to return (default: 100)"),
|
|
52
|
+
excludeSystemTables: z
|
|
53
|
+
.boolean()
|
|
54
|
+
.optional()
|
|
55
|
+
.default(false)
|
|
56
|
+
.describe("Exclude SpatiaLite system tables and indexes from results (default: false)"),
|
|
57
|
+
});
|
|
58
|
+
const VacuumSchema = z.object({
|
|
59
|
+
into: z.string().optional().describe("Optional file path to vacuum into"),
|
|
60
|
+
});
|
|
61
|
+
// New virtual table schemas
|
|
62
|
+
const ListVirtualTablesSchema = z.object({
|
|
63
|
+
pattern: z.string().optional().describe("Optional LIKE pattern to filter"),
|
|
64
|
+
});
|
|
65
|
+
const VirtualTableInfoSchema = z.object({
|
|
66
|
+
tableName: z.string().describe("Name of the virtual table"),
|
|
67
|
+
});
|
|
68
|
+
const DropVirtualTableSchema = z.object({
|
|
69
|
+
tableName: z.string().describe("Name of the virtual table to drop"),
|
|
70
|
+
ifExists: z.boolean().optional().default(true),
|
|
71
|
+
});
|
|
72
|
+
const CreateCsvTableSchema = z.object({
|
|
73
|
+
tableName: z.string().describe("Name for the virtual table"),
|
|
74
|
+
filePath: z.string().describe("Path to the CSV file"),
|
|
75
|
+
header: z.boolean().optional().default(true).describe("First row is header"),
|
|
76
|
+
delimiter: z.string().optional().default(",").describe("Column delimiter"),
|
|
77
|
+
columns: z
|
|
78
|
+
.array(z.string())
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Manual column names if no header"),
|
|
81
|
+
});
|
|
82
|
+
const AnalyzeCsvSchemaSchema = z.object({
|
|
83
|
+
filePath: z.string().describe("Path to the CSV file"),
|
|
84
|
+
sampleRows: z.number().optional().default(100).describe("Rows to sample"),
|
|
85
|
+
delimiter: z.string().optional().default(",").describe("Column delimiter"),
|
|
86
|
+
});
|
|
87
|
+
const CreateRtreeTableSchema = z.object({
|
|
88
|
+
tableName: z.string().describe("Name for the R-Tree table"),
|
|
89
|
+
dimensions: z
|
|
90
|
+
.number()
|
|
91
|
+
.min(2)
|
|
92
|
+
.max(5)
|
|
93
|
+
.optional()
|
|
94
|
+
.default(2)
|
|
95
|
+
.describe("Number of dimensions (2-5)"),
|
|
96
|
+
idColumn: z.string().optional().default("id").describe("ID column name"),
|
|
97
|
+
});
|
|
98
|
+
const CreateSeriesTableSchema = z.object({
|
|
99
|
+
tableName: z.string().describe("Name for the series table"),
|
|
100
|
+
start: z.number().describe("Start value"),
|
|
101
|
+
stop: z.number().describe("Stop value"),
|
|
102
|
+
step: z.number().optional().default(1).describe("Step value"),
|
|
103
|
+
columnName: z.string().optional().default("value").describe("Column name"),
|
|
104
|
+
});
|
|
105
|
+
/**
|
|
106
|
+
* Get all virtual table tools
|
|
107
|
+
*/
|
|
108
|
+
export function getVirtualTools(adapter) {
|
|
109
|
+
return [
|
|
110
|
+
createGenerateSeriesTool(adapter),
|
|
111
|
+
createCreateViewTool(adapter),
|
|
112
|
+
createListViewsTool(adapter),
|
|
113
|
+
createDropViewTool(adapter),
|
|
114
|
+
createDbStatTool(adapter),
|
|
115
|
+
createVacuumTool(adapter),
|
|
116
|
+
// New virtual table tools
|
|
117
|
+
createListVirtualTablesTool(adapter),
|
|
118
|
+
createVirtualTableInfoTool(adapter),
|
|
119
|
+
createDropVirtualTableTool(adapter),
|
|
120
|
+
createCsvTableTool(adapter),
|
|
121
|
+
createAnalyzeCsvSchemaTool(adapter),
|
|
122
|
+
createRtreeTableTool(adapter),
|
|
123
|
+
createSeriesTableTool(adapter),
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate series of numbers
|
|
128
|
+
* Note: Uses pure JS implementation since better-sqlite3's bundled SQLite
|
|
129
|
+
* is not compiled with SQLITE_ENABLE_SERIES (required for native generate_series).
|
|
130
|
+
*/
|
|
131
|
+
function createGenerateSeriesTool(_adapter) {
|
|
132
|
+
return {
|
|
133
|
+
name: "sqlite_generate_series",
|
|
134
|
+
description: "Generate a series of numbers using generate_series() virtual table.",
|
|
135
|
+
group: "admin",
|
|
136
|
+
inputSchema: GenerateSeriesSchema,
|
|
137
|
+
outputSchema: GenerateSeriesOutputSchema,
|
|
138
|
+
requiredScopes: ["read"],
|
|
139
|
+
annotations: readOnly("Generate Series"),
|
|
140
|
+
handler: (params, _context) => {
|
|
141
|
+
const input = GenerateSeriesSchema.parse(params);
|
|
142
|
+
// Generate in JS - better-sqlite3 doesn't include SQLITE_ENABLE_SERIES
|
|
143
|
+
const values = [];
|
|
144
|
+
for (let i = input.start; input.step > 0 ? i <= input.stop : i >= input.stop; i += input.step) {
|
|
145
|
+
values.push(i);
|
|
146
|
+
if (values.length > 10000)
|
|
147
|
+
break; // Safety limit
|
|
148
|
+
}
|
|
149
|
+
return Promise.resolve({
|
|
150
|
+
success: true,
|
|
151
|
+
count: values.length,
|
|
152
|
+
values,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create a view
|
|
159
|
+
*/
|
|
160
|
+
function createCreateViewTool(adapter) {
|
|
161
|
+
return {
|
|
162
|
+
name: "sqlite_create_view",
|
|
163
|
+
description: "Create a view based on a SELECT query.",
|
|
164
|
+
group: "admin",
|
|
165
|
+
inputSchema: CreateViewSchema,
|
|
166
|
+
outputSchema: CreateTableOutputSchema,
|
|
167
|
+
requiredScopes: ["write"],
|
|
168
|
+
annotations: idempotent("Create View"),
|
|
169
|
+
handler: async (params, _context) => {
|
|
170
|
+
const input = CreateViewSchema.parse(params);
|
|
171
|
+
// Validate and quote view name
|
|
172
|
+
const viewName = sanitizeIdentifier(input.viewName);
|
|
173
|
+
// Basic validation that it's a SELECT
|
|
174
|
+
if (!input.selectQuery.trim().toUpperCase().startsWith("SELECT")) {
|
|
175
|
+
throw new Error("View definition must be a SELECT query");
|
|
176
|
+
}
|
|
177
|
+
// SQLite doesn't support CREATE OR REPLACE VIEW
|
|
178
|
+
// Use DROP IF EXISTS + CREATE VIEW pattern instead
|
|
179
|
+
if (input.replace) {
|
|
180
|
+
await adapter.executeQuery(`DROP VIEW IF EXISTS ${viewName}`);
|
|
181
|
+
}
|
|
182
|
+
const sql = `CREATE VIEW ${viewName} AS ${input.selectQuery}`;
|
|
183
|
+
await adapter.executeQuery(sql);
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
message: `View '${input.viewName}' created`,
|
|
187
|
+
sql,
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* List views
|
|
194
|
+
*/
|
|
195
|
+
function createListViewsTool(adapter) {
|
|
196
|
+
return {
|
|
197
|
+
name: "sqlite_list_views",
|
|
198
|
+
description: "List all views in the database.",
|
|
199
|
+
group: "admin",
|
|
200
|
+
inputSchema: ListViewsSchema,
|
|
201
|
+
outputSchema: ListViewsOutputSchema,
|
|
202
|
+
requiredScopes: ["read"],
|
|
203
|
+
annotations: readOnly("List Views"),
|
|
204
|
+
handler: async (params, _context) => {
|
|
205
|
+
const input = ListViewsSchema.parse(params);
|
|
206
|
+
let sql = `SELECT name, sql FROM sqlite_master WHERE type = 'view'`;
|
|
207
|
+
if (input.pattern) {
|
|
208
|
+
const escapedPattern = input.pattern.replace(/'/g, "''");
|
|
209
|
+
sql += ` AND name LIKE '${escapedPattern}'`;
|
|
210
|
+
}
|
|
211
|
+
sql += ` ORDER BY name`;
|
|
212
|
+
const result = await adapter.executeReadQuery(sql);
|
|
213
|
+
let views = (result.rows ?? []).map((row) => ({
|
|
214
|
+
name: typeof row["name"] === "string" ? row["name"] : "",
|
|
215
|
+
sql: typeof row["sql"] === "string" ? row["sql"] : null,
|
|
216
|
+
}));
|
|
217
|
+
// Filter out SpatiaLite system views if requested (default: true)
|
|
218
|
+
if (input.excludeSystemViews) {
|
|
219
|
+
views = views.filter((v) => !isSpatialiteSystemView(v.name));
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
count: views.length,
|
|
224
|
+
views,
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Drop a view
|
|
231
|
+
*/
|
|
232
|
+
function createDropViewTool(adapter) {
|
|
233
|
+
return {
|
|
234
|
+
name: "sqlite_drop_view",
|
|
235
|
+
description: "Drop (delete) a view from the database.",
|
|
236
|
+
group: "admin",
|
|
237
|
+
inputSchema: DropViewSchema,
|
|
238
|
+
outputSchema: DropTableOutputSchema,
|
|
239
|
+
requiredScopes: ["admin"],
|
|
240
|
+
annotations: destructive("Drop View"),
|
|
241
|
+
handler: async (params, _context) => {
|
|
242
|
+
const input = DropViewSchema.parse(params);
|
|
243
|
+
// Validate and quote view name
|
|
244
|
+
const viewName = sanitizeIdentifier(input.viewName);
|
|
245
|
+
const ifExists = input.ifExists ? "IF EXISTS " : "";
|
|
246
|
+
const sql = `DROP VIEW ${ifExists}${viewName}`;
|
|
247
|
+
await adapter.executeQuery(sql);
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
message: `View '${input.viewName}' dropped`,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Database statistics via dbstat
|
|
257
|
+
*/
|
|
258
|
+
function createDbStatTool(adapter) {
|
|
259
|
+
return {
|
|
260
|
+
name: "sqlite_dbstat",
|
|
261
|
+
description: "Get database storage statistics using dbstat virtual table.",
|
|
262
|
+
group: "admin",
|
|
263
|
+
inputSchema: DbStatSchema,
|
|
264
|
+
requiredScopes: ["read"],
|
|
265
|
+
annotations: readOnly("Database Stats"),
|
|
266
|
+
handler: async (params, _context) => {
|
|
267
|
+
const input = DbStatSchema.parse(params);
|
|
268
|
+
try {
|
|
269
|
+
// Summarize mode: aggregate per-table stats
|
|
270
|
+
if (input.summarize) {
|
|
271
|
+
let sql = `SELECT
|
|
272
|
+
name,
|
|
273
|
+
COUNT(*) as page_count,
|
|
274
|
+
SUM(payload) as total_payload,
|
|
275
|
+
SUM(unused) as total_unused,
|
|
276
|
+
SUM(ncell) as total_cells,
|
|
277
|
+
MAX(mx_payload) as max_payload
|
|
278
|
+
FROM dbstat`;
|
|
279
|
+
if (input.table) {
|
|
280
|
+
sanitizeIdentifier(input.table);
|
|
281
|
+
sql += ` WHERE name = '${input.table.replace(/'/g, "''")}'`;
|
|
282
|
+
}
|
|
283
|
+
sql += ` GROUP BY name ORDER BY name LIMIT ${input.limit}`;
|
|
284
|
+
const result = await adapter.executeReadQuery(sql);
|
|
285
|
+
// Filter out SpatiaLite system tables if requested
|
|
286
|
+
let tables = (result.rows ?? []).map((row) => ({
|
|
287
|
+
name: row["name"],
|
|
288
|
+
pageCount: row["page_count"],
|
|
289
|
+
totalPayload: row["total_payload"],
|
|
290
|
+
totalUnused: row["total_unused"],
|
|
291
|
+
totalCells: row["total_cells"],
|
|
292
|
+
maxPayload: row["max_payload"],
|
|
293
|
+
}));
|
|
294
|
+
if (input.excludeSystemTables) {
|
|
295
|
+
tables = tables.filter((t) => !isSpatialiteSystemTable(t.name) &&
|
|
296
|
+
!isSpatialiteSystemIndex(t.name) &&
|
|
297
|
+
// Also filter FTS5 shadow tables (e.g., articles_fts_config, articles_fts_data)
|
|
298
|
+
!t.name.includes("_fts_"));
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
summarized: true,
|
|
303
|
+
objectCount: tables.length,
|
|
304
|
+
objects: tables,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Default mode: raw page-level stats
|
|
308
|
+
let sql = `SELECT name, path, pageno, pagetype, ncell, payload, unused, mx_payload
|
|
309
|
+
FROM dbstat`;
|
|
310
|
+
if (input.table) {
|
|
311
|
+
// Validate table name
|
|
312
|
+
sanitizeIdentifier(input.table);
|
|
313
|
+
// For WHERE clause, we need raw table name without quotes for string comparison
|
|
314
|
+
sql += ` WHERE name = '${input.table.replace(/'/g, "''")}'`;
|
|
315
|
+
}
|
|
316
|
+
sql += ` LIMIT ${input.limit}`;
|
|
317
|
+
const result = await adapter.executeReadQuery(sql);
|
|
318
|
+
// Filter out SpatiaLite system tables/indexes if requested
|
|
319
|
+
let stats = result.rows ?? [];
|
|
320
|
+
if (input.excludeSystemTables) {
|
|
321
|
+
stats = stats.filter((row) => !isSpatialiteSystemTable(row["name"]) &&
|
|
322
|
+
!isSpatialiteSystemIndex(row["name"]) &&
|
|
323
|
+
// Also filter FTS5 shadow tables (e.g., articles_fts_config, articles_fts_data)
|
|
324
|
+
!row["name"].includes("_fts_"));
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
rowCount: stats.length,
|
|
329
|
+
stats,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// dbstat may not be available
|
|
334
|
+
// Fallback to basic stats with optional table-specific estimates
|
|
335
|
+
const pageCountResult = await adapter.executeReadQuery("PRAGMA page_count");
|
|
336
|
+
const totalPageCount = pageCountResult.rows?.[0]?.["page_count"] ??
|
|
337
|
+
pageCountResult.rows?.[0]?.[0];
|
|
338
|
+
const totalPages = typeof totalPageCount === "number"
|
|
339
|
+
? totalPageCount
|
|
340
|
+
: Number(totalPageCount);
|
|
341
|
+
// If a specific table is requested, provide table-specific estimate
|
|
342
|
+
if (input.table) {
|
|
343
|
+
sanitizeIdentifier(input.table);
|
|
344
|
+
const escapedTable = input.table.replace(/'/g, "''");
|
|
345
|
+
// Check if table exists
|
|
346
|
+
const tableCheck = await adapter.executeReadQuery(`SELECT name FROM sqlite_master WHERE type='table' AND name='${escapedTable}'`);
|
|
347
|
+
if (!tableCheck.rows || tableCheck.rows.length === 0) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
message: `Table '${input.table}' not found`,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
// Get row count for the specific table
|
|
354
|
+
const countResult = await adapter.executeReadQuery(`SELECT COUNT(*) as count FROM "${input.table}"`);
|
|
355
|
+
const rowCount = Number(countResult.rows?.[0]?.["count"] ?? 0);
|
|
356
|
+
// Estimate pages: ~100 rows per page for typical data
|
|
357
|
+
const estimatedPages = Math.max(1, Math.ceil(rowCount / 100));
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
table: input.table,
|
|
361
|
+
rowCount,
|
|
362
|
+
estimatedPages,
|
|
363
|
+
totalDatabasePages: totalPages,
|
|
364
|
+
note: "dbstat virtual table not available; page count is estimated from row count (~100 rows/page)",
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
// Get table count for additional context
|
|
368
|
+
const tableCountResult = await adapter.executeReadQuery(`SELECT COUNT(*) as cnt FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
369
|
+
const tableCount = Number(tableCountResult.rows?.[0]?.["cnt"] ?? 0);
|
|
370
|
+
return {
|
|
371
|
+
success: true,
|
|
372
|
+
pageCount: totalPages,
|
|
373
|
+
tableCount,
|
|
374
|
+
note: "dbstat virtual table not available, showing basic stats",
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Vacuum database
|
|
382
|
+
*/
|
|
383
|
+
function createVacuumTool(adapter) {
|
|
384
|
+
return {
|
|
385
|
+
name: "sqlite_vacuum",
|
|
386
|
+
description: "Rebuild the database to reclaim space and optimize structure.",
|
|
387
|
+
group: "admin",
|
|
388
|
+
inputSchema: VacuumSchema,
|
|
389
|
+
outputSchema: VacuumOutputSchema,
|
|
390
|
+
requiredScopes: ["admin"],
|
|
391
|
+
annotations: admin("Vacuum Database"),
|
|
392
|
+
handler: async (params, context) => {
|
|
393
|
+
const input = VacuumSchema.parse(params);
|
|
394
|
+
const progress = buildProgressContext(context);
|
|
395
|
+
// Phase 1: Starting vacuum
|
|
396
|
+
await sendProgress(progress, 1, 2, "Starting vacuum operation...");
|
|
397
|
+
let sql = "VACUUM";
|
|
398
|
+
if (input.into) {
|
|
399
|
+
// VACUUM INTO creates a compacted copy
|
|
400
|
+
const escapedPath = input.into.replace(/'/g, "''");
|
|
401
|
+
sql = `VACUUM INTO '${escapedPath}'`;
|
|
402
|
+
}
|
|
403
|
+
const start = Date.now();
|
|
404
|
+
await adapter.executeQuery(sql);
|
|
405
|
+
const duration = Date.now() - start;
|
|
406
|
+
// Phase 2: Complete
|
|
407
|
+
await sendProgress(progress, 2, 2, "Vacuum complete");
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
message: input.into
|
|
411
|
+
? `Database vacuumed into '${input.into}'`
|
|
412
|
+
: "Database vacuumed",
|
|
413
|
+
durationMs: duration,
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// =============================================================================
|
|
419
|
+
// New Virtual Table Tools
|
|
420
|
+
// =============================================================================
|
|
421
|
+
/**
|
|
422
|
+
* Check if a module is available
|
|
423
|
+
*/
|
|
424
|
+
async function isModuleAvailable(adapter, moduleName) {
|
|
425
|
+
try {
|
|
426
|
+
const result = await adapter.executeReadQuery(`SELECT name FROM pragma_module_list WHERE name = '${moduleName}'`);
|
|
427
|
+
return (result.rows?.length ?? 0) > 0;
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Check if any CSV module is available (standard csv or sqlite-xsv variant)
|
|
435
|
+
*/
|
|
436
|
+
async function isCsvModuleAvailable(adapter) {
|
|
437
|
+
// Check for standard csv module
|
|
438
|
+
if (await isModuleAvailable(adapter, "csv")) {
|
|
439
|
+
return { available: true, variant: "csv" };
|
|
440
|
+
}
|
|
441
|
+
// Check for sqlite-xsv module (registers as xsv_reader)
|
|
442
|
+
if (await isModuleAvailable(adapter, "xsv_reader")) {
|
|
443
|
+
return { available: true, variant: "xsv" };
|
|
444
|
+
}
|
|
445
|
+
// Check for xsv (alternative name)
|
|
446
|
+
if (await isModuleAvailable(adapter, "xsv")) {
|
|
447
|
+
return { available: true, variant: "xsv" };
|
|
448
|
+
}
|
|
449
|
+
return { available: false, variant: null };
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* List all virtual tables
|
|
453
|
+
*/
|
|
454
|
+
function createListVirtualTablesTool(adapter) {
|
|
455
|
+
return {
|
|
456
|
+
name: "sqlite_list_virtual_tables",
|
|
457
|
+
description: "List all virtual tables in the database.",
|
|
458
|
+
group: "admin",
|
|
459
|
+
inputSchema: ListVirtualTablesSchema,
|
|
460
|
+
outputSchema: z.object({
|
|
461
|
+
success: z.boolean(),
|
|
462
|
+
count: z.number(),
|
|
463
|
+
virtualTables: z.array(z.object({
|
|
464
|
+
name: z.string(),
|
|
465
|
+
module: z.string(),
|
|
466
|
+
sql: z.string(),
|
|
467
|
+
})),
|
|
468
|
+
}),
|
|
469
|
+
requiredScopes: ["read"],
|
|
470
|
+
annotations: readOnly("List Virtual Tables"),
|
|
471
|
+
handler: async (params, _context) => {
|
|
472
|
+
const input = ListVirtualTablesSchema.parse(params);
|
|
473
|
+
let sql = `SELECT name, sql FROM sqlite_master WHERE type = 'table' AND sql LIKE 'CREATE VIRTUAL TABLE%'`;
|
|
474
|
+
if (input.pattern) {
|
|
475
|
+
sql += ` AND name LIKE '${input.pattern.replace(/'/g, "''")}'`;
|
|
476
|
+
}
|
|
477
|
+
const result = await adapter.executeReadQuery(sql);
|
|
478
|
+
const virtualTables = (result.rows ?? []).map((row) => {
|
|
479
|
+
const sqlStr = typeof row["sql"] === "string" ? row["sql"] : "";
|
|
480
|
+
// Extract module name from SQL: CREATE VIRTUAL TABLE x USING module(...)
|
|
481
|
+
const match = /USING\s+(\w+)/i.exec(sqlStr);
|
|
482
|
+
return {
|
|
483
|
+
name: typeof row["name"] === "string" ? row["name"] : "",
|
|
484
|
+
module: match?.[1] ?? "unknown",
|
|
485
|
+
sql: sqlStr,
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
return {
|
|
489
|
+
success: true,
|
|
490
|
+
count: virtualTables.length,
|
|
491
|
+
virtualTables,
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get virtual table info
|
|
498
|
+
*/
|
|
499
|
+
function createVirtualTableInfoTool(adapter) {
|
|
500
|
+
return {
|
|
501
|
+
name: "sqlite_virtual_table_info",
|
|
502
|
+
description: "Get metadata about a specific virtual table.",
|
|
503
|
+
group: "admin",
|
|
504
|
+
inputSchema: VirtualTableInfoSchema,
|
|
505
|
+
outputSchema: z.object({
|
|
506
|
+
success: z.boolean(),
|
|
507
|
+
name: z.string(),
|
|
508
|
+
module: z.string(),
|
|
509
|
+
moduleAvailable: z.boolean().optional(),
|
|
510
|
+
columns: z
|
|
511
|
+
.array(z.object({
|
|
512
|
+
name: z.string(),
|
|
513
|
+
type: z.string(),
|
|
514
|
+
}))
|
|
515
|
+
.optional(),
|
|
516
|
+
sql: z.string(),
|
|
517
|
+
note: z.string().optional(),
|
|
518
|
+
}),
|
|
519
|
+
requiredScopes: ["read"],
|
|
520
|
+
annotations: readOnly("Virtual Table Info"),
|
|
521
|
+
handler: async (params, _context) => {
|
|
522
|
+
const input = VirtualTableInfoSchema.parse(params);
|
|
523
|
+
// Validate table name (we need raw name for queries)
|
|
524
|
+
sanitizeIdentifier(input.tableName);
|
|
525
|
+
// Get the CREATE statement
|
|
526
|
+
const sqlResult = await adapter.executeReadQuery(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = '${input.tableName.replace(/'/g, "''")}' AND sql LIKE 'CREATE VIRTUAL TABLE%'`);
|
|
527
|
+
if (!sqlResult.rows || sqlResult.rows.length === 0) {
|
|
528
|
+
throw new Error(`Virtual table '${input.tableName}' not found`);
|
|
529
|
+
}
|
|
530
|
+
const sqlStr = typeof sqlResult.rows[0]?.["sql"] === "string"
|
|
531
|
+
? sqlResult.rows[0]["sql"]
|
|
532
|
+
: "";
|
|
533
|
+
const match = /USING\s+(\w+)/i.exec(sqlStr);
|
|
534
|
+
const moduleName = match?.[1] ?? "unknown";
|
|
535
|
+
// Get column info - may fail if module is unavailable (e.g., FTS5 in WASM)
|
|
536
|
+
try {
|
|
537
|
+
const colResult = await adapter.executeReadQuery(`PRAGMA table_info("${input.tableName}")`);
|
|
538
|
+
const columns = (colResult.rows ?? []).map((row) => ({
|
|
539
|
+
name: typeof row["name"] === "string" ? row["name"] : "",
|
|
540
|
+
type: typeof row["type"] === "string" ? row["type"] : "TEXT",
|
|
541
|
+
}));
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
name: input.tableName,
|
|
545
|
+
module: moduleName,
|
|
546
|
+
moduleAvailable: true,
|
|
547
|
+
columns,
|
|
548
|
+
sql: sqlStr,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
// Module unavailable (e.g., FTS5 in WASM) - return partial info
|
|
553
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
554
|
+
const isModuleError = errMsg.includes("no such module") ||
|
|
555
|
+
errMsg.includes("unknown module");
|
|
556
|
+
if (isModuleError) {
|
|
557
|
+
return {
|
|
558
|
+
success: true,
|
|
559
|
+
name: input.tableName,
|
|
560
|
+
module: moduleName,
|
|
561
|
+
moduleAvailable: false,
|
|
562
|
+
sql: sqlStr,
|
|
563
|
+
note: `Module '${moduleName}' not available in this environment. Column info cannot be retrieved.`,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// Re-throw unexpected errors
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Drop virtual table
|
|
574
|
+
*/
|
|
575
|
+
function createDropVirtualTableTool(adapter) {
|
|
576
|
+
return {
|
|
577
|
+
name: "sqlite_drop_virtual_table",
|
|
578
|
+
description: "Drop a virtual table.",
|
|
579
|
+
group: "admin",
|
|
580
|
+
inputSchema: DropVirtualTableSchema,
|
|
581
|
+
outputSchema: z.object({
|
|
582
|
+
success: z.boolean(),
|
|
583
|
+
message: z.string(),
|
|
584
|
+
}),
|
|
585
|
+
requiredScopes: ["write"],
|
|
586
|
+
annotations: destructive("Drop Virtual Table"),
|
|
587
|
+
handler: async (params, _context) => {
|
|
588
|
+
const input = DropVirtualTableSchema.parse(params);
|
|
589
|
+
// Validate and quote table name
|
|
590
|
+
const tableName = sanitizeIdentifier(input.tableName);
|
|
591
|
+
// Check if the table exists and is a virtual table
|
|
592
|
+
const escapedName = input.tableName.replace(/'/g, "''");
|
|
593
|
+
const existsResult = await adapter.executeReadQuery(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name='${escapedName}'`);
|
|
594
|
+
const tableExists = existsResult.rows !== undefined && existsResult.rows.length > 0;
|
|
595
|
+
const sqlValue = existsResult.rows?.[0]?.["sql"];
|
|
596
|
+
const isVirtualTable = tableExists &&
|
|
597
|
+
typeof sqlValue === "string" &&
|
|
598
|
+
sqlValue.toUpperCase().includes("CREATE VIRTUAL TABLE");
|
|
599
|
+
// Validate that it's actually a virtual table if it exists
|
|
600
|
+
if (tableExists && !isVirtualTable) {
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
message: `'${input.tableName}' is a regular table, not a virtual table. Use sqlite_drop_table instead.`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const sql = input.ifExists
|
|
607
|
+
? `DROP TABLE IF EXISTS ${tableName}`
|
|
608
|
+
: `DROP TABLE ${tableName}`;
|
|
609
|
+
await adapter.executeWriteQuery(sql);
|
|
610
|
+
// Return accurate message based on whether table existed
|
|
611
|
+
if (tableExists) {
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
message: `Dropped virtual table '${input.tableName}'`,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
else if (input.ifExists) {
|
|
618
|
+
return {
|
|
619
|
+
success: true,
|
|
620
|
+
message: `Virtual table '${input.tableName}' did not exist (no action taken)`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
// This shouldn't be reached as DROP TABLE without IF EXISTS would throw
|
|
625
|
+
return {
|
|
626
|
+
success: true,
|
|
627
|
+
message: `Dropped virtual table '${input.tableName}'`,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Create CSV virtual table
|
|
635
|
+
*/
|
|
636
|
+
function createCsvTableTool(adapter) {
|
|
637
|
+
return {
|
|
638
|
+
name: "sqlite_create_csv_table",
|
|
639
|
+
description: "Create a virtual table from a CSV file. Requires the csv extension.",
|
|
640
|
+
group: "admin",
|
|
641
|
+
inputSchema: CreateCsvTableSchema,
|
|
642
|
+
outputSchema: z.object({
|
|
643
|
+
success: z.boolean(),
|
|
644
|
+
message: z.string(),
|
|
645
|
+
sql: z.string(),
|
|
646
|
+
columns: z.array(z.string()),
|
|
647
|
+
}),
|
|
648
|
+
requiredScopes: ["write"],
|
|
649
|
+
annotations: idempotent("Create CSV Table"),
|
|
650
|
+
handler: async (params, _context) => {
|
|
651
|
+
const input = CreateCsvTableSchema.parse(params);
|
|
652
|
+
// Validate table name (we'll use it with double quotes in SQL)
|
|
653
|
+
sanitizeIdentifier(input.tableName);
|
|
654
|
+
// Validate that the file path is absolute (required by SQLite CSV extension)
|
|
655
|
+
if (!path.isAbsolute(input.filePath)) {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
message: `Relative path not supported. Please use an absolute path. Example: ${path.resolve(input.filePath)}`,
|
|
659
|
+
sql: "",
|
|
660
|
+
columns: [],
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
// Check if csv module is available (supports standard csv and sqlite-xsv)
|
|
664
|
+
const { available: csvAvailable } = await isCsvModuleAvailable(adapter);
|
|
665
|
+
if (!csvAvailable) {
|
|
666
|
+
// Check if we're in WASM mode by testing for a WASM-specific limitation
|
|
667
|
+
const isWasm = !(await isModuleAvailable(adapter, "rtree"));
|
|
668
|
+
return {
|
|
669
|
+
success: false,
|
|
670
|
+
message: isWasm
|
|
671
|
+
? "CSV extension not available in WASM mode. Use native SQLite with the csv extension."
|
|
672
|
+
: "CSV extension not available. Load the csv/xsv extension using --csv flag or set CSV_EXTENSION_PATH.",
|
|
673
|
+
sql: "",
|
|
674
|
+
columns: [],
|
|
675
|
+
wasmLimitation: isWasm,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
// Build CREATE VIRTUAL TABLE statement
|
|
679
|
+
const options = [
|
|
680
|
+
`filename='${input.filePath.replace(/'/g, "''")}'`,
|
|
681
|
+
];
|
|
682
|
+
if (!input.header) {
|
|
683
|
+
options.push("header=false");
|
|
684
|
+
}
|
|
685
|
+
if (input.delimiter !== ",") {
|
|
686
|
+
options.push(`delimiter='${input.delimiter}'`);
|
|
687
|
+
}
|
|
688
|
+
if (input.columns && input.columns.length > 0) {
|
|
689
|
+
options.push(`columns=${String(input.columns.length)}`);
|
|
690
|
+
}
|
|
691
|
+
const sql = `CREATE VIRTUAL TABLE "${input.tableName}" USING csv(${options.join(", ")})`;
|
|
692
|
+
await adapter.executeWriteQuery(sql);
|
|
693
|
+
// Get column names
|
|
694
|
+
const colResult = await adapter.executeReadQuery(`PRAGMA table_info("${input.tableName}")`);
|
|
695
|
+
const columns = (colResult.rows ?? []).map((row) => typeof row["name"] === "string" ? row["name"] : "");
|
|
696
|
+
return {
|
|
697
|
+
success: true,
|
|
698
|
+
message: `Created CSV virtual table '${input.tableName}'`,
|
|
699
|
+
sql,
|
|
700
|
+
columns,
|
|
701
|
+
};
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Analyze CSV schema
|
|
707
|
+
*/
|
|
708
|
+
function createAnalyzeCsvSchemaTool(adapter) {
|
|
709
|
+
return {
|
|
710
|
+
name: "sqlite_analyze_csv_schema",
|
|
711
|
+
description: "Analyze a CSV file structure and infer column types. Uses a temporary virtual table.",
|
|
712
|
+
group: "admin",
|
|
713
|
+
inputSchema: AnalyzeCsvSchemaSchema,
|
|
714
|
+
outputSchema: z.object({
|
|
715
|
+
success: z.boolean(),
|
|
716
|
+
hasHeader: z.boolean(),
|
|
717
|
+
rowCount: z.number(),
|
|
718
|
+
columns: z.array(z.object({
|
|
719
|
+
name: z.string(),
|
|
720
|
+
inferredType: z.string(),
|
|
721
|
+
nullCount: z.number(),
|
|
722
|
+
sampleValues: z.array(z.string()),
|
|
723
|
+
})),
|
|
724
|
+
}),
|
|
725
|
+
requiredScopes: ["read"],
|
|
726
|
+
annotations: readOnly("Analyze CSV Schema"),
|
|
727
|
+
handler: async (params, _context) => {
|
|
728
|
+
const input = AnalyzeCsvSchemaSchema.parse(params);
|
|
729
|
+
// Validate that the file path is absolute (required by SQLite CSV extension)
|
|
730
|
+
if (!path.isAbsolute(input.filePath)) {
|
|
731
|
+
return {
|
|
732
|
+
success: false,
|
|
733
|
+
message: `Relative path not supported. Please use an absolute path. Example: ${path.resolve(input.filePath)}`,
|
|
734
|
+
hasHeader: false,
|
|
735
|
+
rowCount: 0,
|
|
736
|
+
columns: [],
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
// Check if csv module is available (supports standard csv and sqlite-xsv)
|
|
740
|
+
const { available: csvAvailable } = await isCsvModuleAvailable(adapter);
|
|
741
|
+
if (!csvAvailable) {
|
|
742
|
+
// Check if we're in WASM mode by testing for a WASM-specific limitation
|
|
743
|
+
const isWasm = !(await isModuleAvailable(adapter, "rtree"));
|
|
744
|
+
return {
|
|
745
|
+
success: false,
|
|
746
|
+
message: isWasm
|
|
747
|
+
? "CSV extension not available in WASM mode. Use native SQLite with the csv extension."
|
|
748
|
+
: "CSV extension not available. Load the csv/xsv extension using --csv flag or set CSV_EXTENSION_PATH.",
|
|
749
|
+
hasHeader: false,
|
|
750
|
+
rowCount: 0,
|
|
751
|
+
columns: [],
|
|
752
|
+
wasmLimitation: isWasm,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
// Create temporary table name
|
|
756
|
+
const tempName = `_csv_analyze_${Date.now()}`;
|
|
757
|
+
try {
|
|
758
|
+
// Create temp virtual table
|
|
759
|
+
const options = [`filename='${input.filePath.replace(/'/g, "''")}'`];
|
|
760
|
+
if (input.delimiter !== ",") {
|
|
761
|
+
options.push(`delimiter='${input.delimiter}'`);
|
|
762
|
+
}
|
|
763
|
+
await adapter.executeWriteQuery(`CREATE VIRTUAL TABLE "${tempName}" USING csv(${options.join(", ")})`);
|
|
764
|
+
// Get columns
|
|
765
|
+
const colResult = await adapter.executeReadQuery(`PRAGMA table_info("${tempName}")`);
|
|
766
|
+
const columnNames = (colResult.rows ?? []).map((row) => {
|
|
767
|
+
const name = row["name"];
|
|
768
|
+
const cid = row["cid"];
|
|
769
|
+
if (typeof name === "string")
|
|
770
|
+
return name;
|
|
771
|
+
return `col${typeof cid === "number" ? cid : 0}`;
|
|
772
|
+
});
|
|
773
|
+
// Sample data
|
|
774
|
+
const sampleResult = await adapter.executeReadQuery(`SELECT * FROM "${tempName}" LIMIT ${input.sampleRows}`);
|
|
775
|
+
// Analyze each column
|
|
776
|
+
const columns = columnNames.map((name) => {
|
|
777
|
+
let nullCount = 0;
|
|
778
|
+
let intCount = 0;
|
|
779
|
+
let floatCount = 0;
|
|
780
|
+
const samples = [];
|
|
781
|
+
for (const row of sampleResult.rows ?? []) {
|
|
782
|
+
const val = row[name];
|
|
783
|
+
const strVal = typeof val === "string" ? val : JSON.stringify(val ?? "");
|
|
784
|
+
if (val === null || strVal === "") {
|
|
785
|
+
nullCount++;
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
if (samples.length < 3)
|
|
789
|
+
samples.push(strVal);
|
|
790
|
+
if (/^-?\d+$/.test(strVal))
|
|
791
|
+
intCount++;
|
|
792
|
+
else if (/^-?\d+\.\d+$/.test(strVal))
|
|
793
|
+
floatCount++;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const total = (sampleResult.rows?.length ?? 0) - nullCount;
|
|
797
|
+
let inferredType = "TEXT";
|
|
798
|
+
if (total > 0) {
|
|
799
|
+
if (intCount === total)
|
|
800
|
+
inferredType = "INTEGER";
|
|
801
|
+
else if (floatCount === total || intCount + floatCount === total)
|
|
802
|
+
inferredType = "REAL";
|
|
803
|
+
}
|
|
804
|
+
return { name, inferredType, nullCount, sampleValues: samples };
|
|
805
|
+
});
|
|
806
|
+
// Count total rows
|
|
807
|
+
const countResult = await adapter.executeReadQuery(`SELECT COUNT(*) as cnt FROM "${tempName}"`);
|
|
808
|
+
const rowCount = typeof countResult.rows?.[0]?.["cnt"] === "number"
|
|
809
|
+
? countResult.rows[0]["cnt"]
|
|
810
|
+
: 0;
|
|
811
|
+
return {
|
|
812
|
+
success: true,
|
|
813
|
+
hasHeader: true, // CSV module assumes header
|
|
814
|
+
rowCount,
|
|
815
|
+
columns,
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
finally {
|
|
819
|
+
// Clean up temp table
|
|
820
|
+
await adapter
|
|
821
|
+
.executeWriteQuery(`DROP TABLE IF EXISTS "${tempName}"`)
|
|
822
|
+
.catch(() => {
|
|
823
|
+
// Ignore cleanup errors
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Create R-Tree virtual table
|
|
831
|
+
*/
|
|
832
|
+
function createRtreeTableTool(adapter) {
|
|
833
|
+
return {
|
|
834
|
+
name: "sqlite_create_rtree_table",
|
|
835
|
+
description: "Create an R-Tree virtual table for spatial indexing. Supports 2-5 dimensions.",
|
|
836
|
+
group: "admin",
|
|
837
|
+
inputSchema: CreateRtreeTableSchema,
|
|
838
|
+
outputSchema: z.object({
|
|
839
|
+
success: z.boolean(),
|
|
840
|
+
message: z.string(),
|
|
841
|
+
sql: z.string(),
|
|
842
|
+
columns: z.array(z.string()),
|
|
843
|
+
}),
|
|
844
|
+
requiredScopes: ["write"],
|
|
845
|
+
annotations: idempotent("Create R-Tree Table"),
|
|
846
|
+
handler: async (params, _context) => {
|
|
847
|
+
const input = CreateRtreeTableSchema.parse(params);
|
|
848
|
+
// Validate table name
|
|
849
|
+
sanitizeIdentifier(input.tableName);
|
|
850
|
+
// Check if rtree module is available
|
|
851
|
+
const rtreeAvailable = await isModuleAvailable(adapter, "rtree");
|
|
852
|
+
if (!rtreeAvailable) {
|
|
853
|
+
return {
|
|
854
|
+
success: false,
|
|
855
|
+
message: "R-Tree extension not available. Use a SQLite build with rtree support.",
|
|
856
|
+
sql: "",
|
|
857
|
+
columns: [],
|
|
858
|
+
wasmLimitation: true,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
// Build column list based on dimensions
|
|
862
|
+
const columns = [input.idColumn];
|
|
863
|
+
const dimNames = ["X", "Y", "Z", "W", "V"];
|
|
864
|
+
for (let i = 0; i < input.dimensions; i++) {
|
|
865
|
+
const dim = dimNames[i] ?? `D${i}`;
|
|
866
|
+
columns.push(`min${dim}`, `max${dim}`);
|
|
867
|
+
}
|
|
868
|
+
const sql = `CREATE VIRTUAL TABLE "${input.tableName}" USING rtree(${columns.join(", ")})`;
|
|
869
|
+
await adapter.executeWriteQuery(sql);
|
|
870
|
+
return {
|
|
871
|
+
success: true,
|
|
872
|
+
message: `Created R-Tree table '${input.tableName}' with ${input.dimensions} dimensions`,
|
|
873
|
+
sql,
|
|
874
|
+
columns,
|
|
875
|
+
};
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Create persistent series table
|
|
881
|
+
*/
|
|
882
|
+
function createSeriesTableTool(adapter) {
|
|
883
|
+
return {
|
|
884
|
+
name: "sqlite_create_series_table",
|
|
885
|
+
description: "Create a table populated with a series of numbers. Unlike generate_series, this creates a persistent table.",
|
|
886
|
+
group: "admin",
|
|
887
|
+
inputSchema: CreateSeriesTableSchema,
|
|
888
|
+
outputSchema: z.object({
|
|
889
|
+
success: z.boolean(),
|
|
890
|
+
message: z.string(),
|
|
891
|
+
rowCount: z.number(),
|
|
892
|
+
}),
|
|
893
|
+
requiredScopes: ["write"],
|
|
894
|
+
annotations: idempotent("Create Series Table"),
|
|
895
|
+
handler: async (params, _context) => {
|
|
896
|
+
const input = CreateSeriesTableSchema.parse(params);
|
|
897
|
+
// Validate and quote identifiers
|
|
898
|
+
const tableName = sanitizeIdentifier(input.tableName);
|
|
899
|
+
const columnName = sanitizeIdentifier(input.columnName);
|
|
900
|
+
// Create table
|
|
901
|
+
await adapter.executeWriteQuery(`CREATE TABLE IF NOT EXISTS ${tableName} (${columnName} INTEGER PRIMARY KEY)`);
|
|
902
|
+
// Insert series using generate_series if available, otherwise loop
|
|
903
|
+
try {
|
|
904
|
+
await adapter.executeWriteQuery(`INSERT OR IGNORE INTO ${tableName} (${columnName}) SELECT value FROM generate_series(${input.start}, ${input.stop}, ${input.step})`);
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
// Fallback: insert values manually
|
|
908
|
+
const values = [];
|
|
909
|
+
for (let v = input.start; v <= input.stop; v += input.step) {
|
|
910
|
+
values.push(v);
|
|
911
|
+
}
|
|
912
|
+
if (values.length > 0) {
|
|
913
|
+
const insertValues = values.map((v) => `(${v})`).join(",");
|
|
914
|
+
await adapter.executeWriteQuery(`INSERT OR IGNORE INTO ${tableName} (${columnName}) VALUES ${insertValues}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// Count rows
|
|
918
|
+
const countResult = await adapter.executeReadQuery(`SELECT COUNT(*) as cnt FROM ${tableName}`);
|
|
919
|
+
const rowCount = typeof countResult.rows?.[0]?.["cnt"] === "number"
|
|
920
|
+
? countResult.rows[0]["cnt"]
|
|
921
|
+
: 0;
|
|
922
|
+
return {
|
|
923
|
+
success: true,
|
|
924
|
+
message: `Created series table '${input.tableName}' with ${rowCount} rows`,
|
|
925
|
+
rowCount,
|
|
926
|
+
};
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
//# sourceMappingURL=virtual.js.map
|