db-mcp 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/LICENSE +1 -1
- package/README.md +244 -518
- package/dist/chunk-4IA3DB5C.js +135 -0
- package/dist/chunk-AOUL5SHS.js +930 -0
- package/dist/chunk-DWJXQEFA.js +13759 -0
- package/dist/chunk-DZQLDEQS.js +879 -0
- package/dist/chunk-S5IDDSSB.js +74 -0
- package/dist/chunk-Z2GFQU3G.js +363 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +227 -183
- package/dist/http-VSB7DBJR.js +1541 -0
- package/dist/index.d.ts +842 -10
- package/dist/index.js +5 -17
- package/dist/sqlite-VHBA4ABV.js +600 -0
- package/dist/sqlite-native-D2LH5ZT7.js +2498 -0
- package/dist/worker-script.d.ts +2 -0
- package/dist/worker-script.js +126 -0
- package/package.json +26 -16
- package/playwright.config.ts +101 -0
- package/scripts/generate-server-instructions.ts +111 -0
- package/server.json +5 -6
- package/test-server/README.md +118 -0
- package/test-server/code-map.md +409 -0
- package/test-server/fixtures/sample.csv +6 -0
- package/test-server/reset-database.ps1 +373 -0
- package/test-server/sample.csv +11 -0
- package/test-server/test-agent-experience.md +243 -0
- package/test-server/test-database.sql +388 -0
- package/test-server/test-group-tools.md +861 -0
- package/test-server/test-help-resources.mjs +238 -0
- package/test-server/test-preflight.md +53 -0
- package/test-server/test-prompts.md +354 -0
- package/test-server/test-resources.md +245 -0
- package/test-server/test-tool-annotations.mjs +157 -0
- package/test-server/test-tools-advanced-1.md +517 -0
- package/test-server/test-tools-advanced-2.md +487 -0
- package/test-server/test-tools-codemode.md +629 -0
- package/test-server/test-tools.md +176 -0
- package/test-server/tool-reference.md +236 -0
- package/tsconfig.test.json +9 -0
- package/dist/adapters/DatabaseAdapter.d.ts +0 -141
- package/dist/adapters/DatabaseAdapter.d.ts.map +0 -1
- package/dist/adapters/DatabaseAdapter.js +0 -131
- package/dist/adapters/DatabaseAdapter.js.map +0 -1
- package/dist/adapters/sqlite/SchemaManager.d.ts +0 -58
- package/dist/adapters/sqlite/SchemaManager.d.ts.map +0 -1
- package/dist/adapters/sqlite/SchemaManager.js +0 -187
- package/dist/adapters/sqlite/SchemaManager.js.map +0 -1
- package/dist/adapters/sqlite/SqliteAdapter.d.ts +0 -161
- package/dist/adapters/sqlite/SqliteAdapter.d.ts.map +0 -1
- package/dist/adapters/sqlite/SqliteAdapter.js +0 -741
- package/dist/adapters/sqlite/SqliteAdapter.js.map +0 -1
- package/dist/adapters/sqlite/index.d.ts +0 -9
- package/dist/adapters/sqlite/index.d.ts.map +0 -1
- package/dist/adapters/sqlite/index.js +0 -8
- package/dist/adapters/sqlite/index.js.map +0 -1
- package/dist/adapters/sqlite/json-utils.d.ts +0 -100
- package/dist/adapters/sqlite/json-utils.d.ts.map +0 -1
- package/dist/adapters/sqlite/json-utils.js +0 -274
- package/dist/adapters/sqlite/json-utils.js.map +0 -1
- package/dist/adapters/sqlite/output-schemas.d.ts +0 -1187
- package/dist/adapters/sqlite/output-schemas.d.ts.map +0 -1
- package/dist/adapters/sqlite/output-schemas.js +0 -1337
- package/dist/adapters/sqlite/output-schemas.js.map +0 -1
- package/dist/adapters/sqlite/prompts.d.ts +0 -13
- package/dist/adapters/sqlite/prompts.d.ts.map +0 -1
- package/dist/adapters/sqlite/prompts.js +0 -605
- package/dist/adapters/sqlite/prompts.js.map +0 -1
- package/dist/adapters/sqlite/resources.d.ts +0 -13
- package/dist/adapters/sqlite/resources.d.ts.map +0 -1
- package/dist/adapters/sqlite/resources.js +0 -251
- package/dist/adapters/sqlite/resources.js.map +0 -1
- package/dist/adapters/sqlite/tools/admin.d.ts +0 -14
- package/dist/adapters/sqlite/tools/admin.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/admin.js +0 -788
- package/dist/adapters/sqlite/tools/admin.js.map +0 -1
- package/dist/adapters/sqlite/tools/core.d.ts +0 -25
- package/dist/adapters/sqlite/tools/core.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/core.js +0 -359
- package/dist/adapters/sqlite/tools/core.js.map +0 -1
- package/dist/adapters/sqlite/tools/fts.d.ts +0 -13
- package/dist/adapters/sqlite/tools/fts.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/fts.js +0 -347
- package/dist/adapters/sqlite/tools/fts.js.map +0 -1
- package/dist/adapters/sqlite/tools/geo.d.ts +0 -14
- package/dist/adapters/sqlite/tools/geo.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/geo.js +0 -252
- package/dist/adapters/sqlite/tools/geo.js.map +0 -1
- package/dist/adapters/sqlite/tools/index.d.ts +0 -30
- package/dist/adapters/sqlite/tools/index.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/index.js +0 -61
- package/dist/adapters/sqlite/tools/index.js.map +0 -1
- package/dist/adapters/sqlite/tools/json-helpers.d.ts +0 -14
- package/dist/adapters/sqlite/tools/json-helpers.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/json-helpers.js +0 -477
- package/dist/adapters/sqlite/tools/json-helpers.js.map +0 -1
- package/dist/adapters/sqlite/tools/json-operations.d.ts +0 -14
- package/dist/adapters/sqlite/tools/json-operations.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/json-operations.js +0 -839
- package/dist/adapters/sqlite/tools/json-operations.js.map +0 -1
- package/dist/adapters/sqlite/tools/stats.d.ts +0 -15
- package/dist/adapters/sqlite/tools/stats.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/stats.js +0 -1219
- package/dist/adapters/sqlite/tools/stats.js.map +0 -1
- package/dist/adapters/sqlite/tools/text.d.ts +0 -14
- package/dist/adapters/sqlite/tools/text.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/text.js +0 -1141
- package/dist/adapters/sqlite/tools/text.js.map +0 -1
- package/dist/adapters/sqlite/tools/vector.d.ts +0 -14
- package/dist/adapters/sqlite/tools/vector.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/vector.js +0 -613
- package/dist/adapters/sqlite/tools/vector.js.map +0 -1
- package/dist/adapters/sqlite/tools/virtual.d.ts +0 -13
- package/dist/adapters/sqlite/tools/virtual.d.ts.map +0 -1
- package/dist/adapters/sqlite/tools/virtual.js +0 -930
- package/dist/adapters/sqlite/tools/virtual.js.map +0 -1
- package/dist/adapters/sqlite/types.d.ts +0 -207
- package/dist/adapters/sqlite/types.d.ts.map +0 -1
- package/dist/adapters/sqlite/types.js +0 -186
- package/dist/adapters/sqlite/types.js.map +0 -1
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts +0 -163
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js +0 -748
- package/dist/adapters/sqlite-native/NativeSqliteAdapter.js.map +0 -1
- package/dist/adapters/sqlite-native/index.d.ts +0 -11
- package/dist/adapters/sqlite-native/index.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/index.js +0 -11
- package/dist/adapters/sqlite-native/index.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts +0 -19
- package/dist/adapters/sqlite-native/tools/spatialite.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/spatialite.js +0 -628
- package/dist/adapters/sqlite-native/tools/spatialite.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/transactions.d.ts +0 -12
- package/dist/adapters/sqlite-native/tools/transactions.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/transactions.js +0 -255
- package/dist/adapters/sqlite-native/tools/transactions.js.map +0 -1
- package/dist/adapters/sqlite-native/tools/window.d.ts +0 -12
- package/dist/adapters/sqlite-native/tools/window.d.ts.map +0 -1
- package/dist/adapters/sqlite-native/tools/window.js +0 -370
- package/dist/adapters/sqlite-native/tools/window.js.map +0 -1
- package/dist/auth/AuthorizationServerDiscovery.d.ts +0 -90
- package/dist/auth/AuthorizationServerDiscovery.d.ts.map +0 -1
- package/dist/auth/AuthorizationServerDiscovery.js +0 -204
- package/dist/auth/AuthorizationServerDiscovery.js.map +0 -1
- package/dist/auth/OAuthResourceServer.d.ts +0 -65
- package/dist/auth/OAuthResourceServer.d.ts.map +0 -1
- package/dist/auth/OAuthResourceServer.js +0 -121
- package/dist/auth/OAuthResourceServer.js.map +0 -1
- package/dist/auth/TokenValidator.d.ts +0 -60
- package/dist/auth/TokenValidator.d.ts.map +0 -1
- package/dist/auth/TokenValidator.js +0 -235
- package/dist/auth/TokenValidator.js.map +0 -1
- package/dist/auth/errors.d.ts +0 -74
- package/dist/auth/errors.d.ts.map +0 -1
- package/dist/auth/errors.js +0 -133
- package/dist/auth/errors.js.map +0 -1
- package/dist/auth/index.d.ts +0 -13
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -15
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/middleware.d.ts +0 -81
- package/dist/auth/middleware.d.ts.map +0 -1
- package/dist/auth/middleware.js +0 -291
- package/dist/auth/middleware.js.map +0 -1
- package/dist/auth/scopes.d.ts +0 -136
- package/dist/auth/scopes.d.ts.map +0 -1
- package/dist/auth/scopes.js +0 -349
- package/dist/auth/scopes.js.map +0 -1
- package/dist/auth/types.d.ts +0 -257
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/auth/types.js +0 -8
- package/dist/auth/types.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/constants/ServerInstructions.d.ts +0 -45
- package/dist/constants/ServerInstructions.d.ts.map +0 -1
- package/dist/constants/ServerInstructions.js +0 -356
- package/dist/constants/ServerInstructions.js.map +0 -1
- package/dist/filtering/ToolConstants.d.ts +0 -34
- package/dist/filtering/ToolConstants.d.ts.map +0 -1
- package/dist/filtering/ToolConstants.js +0 -174
- package/dist/filtering/ToolConstants.js.map +0 -1
- package/dist/filtering/ToolFilter.d.ts +0 -82
- package/dist/filtering/ToolFilter.d.ts.map +0 -1
- package/dist/filtering/ToolFilter.js +0 -296
- package/dist/filtering/ToolFilter.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/server/McpServer.d.ts +0 -61
- package/dist/server/McpServer.d.ts.map +0 -1
- package/dist/server/McpServer.js +0 -270
- package/dist/server/McpServer.js.map +0 -1
- package/dist/transports/http.d.ts +0 -134
- package/dist/transports/http.d.ts.map +0 -1
- package/dist/transports/http.js +0 -516
- package/dist/transports/http.js.map +0 -1
- package/dist/transports/index.d.ts +0 -5
- package/dist/transports/index.d.ts.map +0 -1
- package/dist/transports/index.js +0 -5
- package/dist/transports/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -380
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -68
- package/dist/types/index.js.map +0 -1
- package/dist/utils/annotations.d.ts +0 -44
- package/dist/utils/annotations.d.ts.map +0 -1
- package/dist/utils/annotations.js +0 -77
- package/dist/utils/annotations.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -155
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -329
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/identifiers.d.ts +0 -121
- package/dist/utils/identifiers.d.ts.map +0 -1
- package/dist/utils/identifiers.js +0 -319
- package/dist/utils/identifiers.js.map +0 -1
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -7
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/insightsManager.d.ts +0 -39
- package/dist/utils/insightsManager.d.ts.map +0 -1
- package/dist/utils/insightsManager.js +0 -63
- package/dist/utils/insightsManager.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -189
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -394
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/progress-utils.d.ts +0 -54
- package/dist/utils/progress-utils.d.ts.map +0 -1
- package/dist/utils/progress-utils.js +0 -74
- package/dist/utils/progress-utils.js.map +0 -1
- package/dist/utils/resourceAnnotations.d.ts +0 -36
- package/dist/utils/resourceAnnotations.d.ts.map +0 -1
- package/dist/utils/resourceAnnotations.js +0 -57
- package/dist/utils/resourceAnnotations.js.map +0 -1
- package/dist/utils/where-clause.d.ts +0 -41
- package/dist/utils/where-clause.d.ts.map +0 -1
- package/dist/utils/where-clause.js +0 -116
- package/dist/utils/where-clause.js.map +0 -1
|
@@ -0,0 +1,2498 @@
|
|
|
1
|
+
import { TransactionBeginOutputSchema, TransactionCommitOutputSchema, TransactionRollbackOutputSchema, TransactionSavepointOutputSchema, TransactionReleaseOutputSchema, TransactionRollbackToOutputSchema, TransactionExecuteOutputSchema, WindowRowNumberOutputSchema, validateWhereClause, WindowRankOutputSchema, WindowLagLeadOutputSchema, WindowRunningTotalOutputSchema, resolveAliases, WindowMovingAvgOutputSchema, WindowNtileOutputSchema, autoEnableWal, detectAndSetJsonbSupport, SchemaManager, applyCommonPragmas, isDDL, getResourceDefinitions, getPromptDefinitions, SpatialiteLoadOutputSchema, SpatialiteCreateTableOutputSchema, SpatialiteQueryOutputSchema, SpatialiteAnalyzeOutputSchema, SpatialiteIndexOutputSchema, SpatialiteTransformOutputSchema, SpatialiteImportOutputSchema, normalizeSqliteParams, getCoreTools, getJsonOperationTools, getJsonHelperTools, getTextTools, getStatsTools, getVirtualTools, getVectorTools, getGeoTools, getAdminTools, getIntrospectionTools, getMigrationTools, getCodeModeTools, FtsCreateOutputSchema, sanitizeIdentifier, FtsSearchOutputSchema, validateTableExists, FtsRebuildOutputSchema } from './chunk-DWJXQEFA.js';
|
|
2
|
+
import { DatabaseAdapter } from './chunk-Z2GFQU3G.js';
|
|
3
|
+
import { write, readOnly, admin, getToolGroupIcon, idempotent } from './chunk-S5IDDSSB.js';
|
|
4
|
+
import { formatHandlerError, ValidationError, ConfigurationError, ConnectionError, ResourceNotFoundError, QueryError } from './chunk-4IA3DB5C.js';
|
|
5
|
+
import { logger, ERROR_CODES, DbMcpError } from './chunk-AOUL5SHS.js';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
function buildFts5UnavailableError(tableName) {
|
|
13
|
+
return {
|
|
14
|
+
success: false,
|
|
15
|
+
message: "FTS5 module unavailable",
|
|
16
|
+
tableName: tableName ?? "",
|
|
17
|
+
error: "FTS5 module unavailable",
|
|
18
|
+
hint: "FTS5 requires native SQLite backend (--sqlite-native). WASM mode (sql.js) does not include this module."
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isFts5UnavailableError(error) {
|
|
22
|
+
return error instanceof Error && error.message.toLowerCase().includes("no such module: fts5");
|
|
23
|
+
}
|
|
24
|
+
function isFts5Available(adapter) {
|
|
25
|
+
return adapter.isNativeBackend();
|
|
26
|
+
}
|
|
27
|
+
function buildFts5SearchUnavailableError() {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
rowCount: 0,
|
|
31
|
+
results: [],
|
|
32
|
+
error: "FTS5 module unavailable",
|
|
33
|
+
hint: "FTS5 requires native SQLite backend (--sqlite-native). WASM mode (sql.js) does not include this module."
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var coerceEnumValues = (allowed) => (val) => typeof val === "string" && allowed.includes(val) ? val : void 0;
|
|
37
|
+
var FtsCreateSchema = z.object({
|
|
38
|
+
tableName: z.string().describe("Name of the FTS table to create"),
|
|
39
|
+
sourceTable: z.string().describe("Source table to index"),
|
|
40
|
+
columns: z.array(z.string()).describe("Columns to include in the index"),
|
|
41
|
+
contentTable: z.string().optional().describe("Content table for external content FTS"),
|
|
42
|
+
tokenizer: z.preprocess(
|
|
43
|
+
coerceEnumValues(["unicode61", "ascii", "porter"]),
|
|
44
|
+
z.enum(["unicode61", "ascii", "porter"]).optional().default("unicode61")
|
|
45
|
+
),
|
|
46
|
+
createTriggers: z.boolean().optional().default(true).describe(
|
|
47
|
+
"Create INSERT/UPDATE/DELETE triggers for auto-sync (only for external content tables)"
|
|
48
|
+
)
|
|
49
|
+
});
|
|
50
|
+
var FtsSearchSchema = z.object({
|
|
51
|
+
table: z.string().describe("FTS table name"),
|
|
52
|
+
query: z.string().describe("Full-text search query"),
|
|
53
|
+
columns: z.array(z.string()).optional().describe("Specific columns to search"),
|
|
54
|
+
limit: z.preprocess(
|
|
55
|
+
(val) => typeof val === "string" ? isNaN(Number(val)) ? void 0 : Number(val) : val,
|
|
56
|
+
z.number().optional().default(100)
|
|
57
|
+
),
|
|
58
|
+
highlight: z.boolean().optional().default(false).describe("Include highlighted snippets")
|
|
59
|
+
});
|
|
60
|
+
var FtsRebuildSchema = z.object({
|
|
61
|
+
table: z.string().describe("FTS table name to rebuild")
|
|
62
|
+
});
|
|
63
|
+
var FtsMatchInfoSchema = z.object({
|
|
64
|
+
table: z.string().describe("FTS table name"),
|
|
65
|
+
query: z.string().describe("Full-text search query"),
|
|
66
|
+
format: z.preprocess(
|
|
67
|
+
coerceEnumValues(["bm25", "rank"]),
|
|
68
|
+
z.enum(["bm25", "rank"]).optional().default("bm25")
|
|
69
|
+
)
|
|
70
|
+
});
|
|
71
|
+
function getFtsTools(adapter) {
|
|
72
|
+
return [
|
|
73
|
+
createFtsCreateTool(adapter),
|
|
74
|
+
createFtsSearchTool(adapter),
|
|
75
|
+
createFtsRebuildTool(adapter),
|
|
76
|
+
createFtsMatchInfoTool(adapter)
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
function createFtsCreateTool(adapter) {
|
|
80
|
+
return {
|
|
81
|
+
name: "sqlite_fts_create",
|
|
82
|
+
description: "Create an FTS5 full-text search virtual table.",
|
|
83
|
+
group: "text",
|
|
84
|
+
inputSchema: FtsCreateSchema,
|
|
85
|
+
outputSchema: FtsCreateOutputSchema,
|
|
86
|
+
requiredScopes: ["write"],
|
|
87
|
+
annotations: idempotent("FTS Create"),
|
|
88
|
+
handler: async (params, _context) => {
|
|
89
|
+
try {
|
|
90
|
+
const input = FtsCreateSchema.parse(params);
|
|
91
|
+
sanitizeIdentifier(input.tableName);
|
|
92
|
+
sanitizeIdentifier(input.sourceTable);
|
|
93
|
+
for (const col of input.columns) {
|
|
94
|
+
sanitizeIdentifier(col);
|
|
95
|
+
}
|
|
96
|
+
const columnList = input.columns.join(", ");
|
|
97
|
+
let options = `tokenize="${input.tokenizer}"`;
|
|
98
|
+
const effectiveContentTable = input.contentTable ?? input.sourceTable;
|
|
99
|
+
sanitizeIdentifier(effectiveContentTable);
|
|
100
|
+
options += `, content="${effectiveContentTable}"`;
|
|
101
|
+
const sql = `CREATE VIRTUAL TABLE IF NOT EXISTS "${input.tableName}" USING fts5(${columnList}, ${options})`;
|
|
102
|
+
await adapter.executeQuery(sql);
|
|
103
|
+
let triggersCreated = [];
|
|
104
|
+
if (input.createTriggers) {
|
|
105
|
+
triggersCreated = await createSyncTriggers(
|
|
106
|
+
adapter,
|
|
107
|
+
input.tableName,
|
|
108
|
+
effectiveContentTable,
|
|
109
|
+
input.columns
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
await adapter.executeQuery(
|
|
113
|
+
`INSERT INTO "${input.tableName}"("${input.tableName}") VALUES('rebuild')`
|
|
114
|
+
);
|
|
115
|
+
const message = triggersCreated.length ? `FTS5 table '${input.tableName}' created with ${triggersCreated.length} sync triggers` : `FTS5 table '${input.tableName}' created`;
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
message,
|
|
119
|
+
tableName: input.tableName,
|
|
120
|
+
triggersCreated: triggersCreated.length ? triggersCreated : void 0
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (isFts5UnavailableError(error)) {
|
|
124
|
+
return buildFts5UnavailableError(
|
|
125
|
+
params?.tableName
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return formatHandlerError(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function createSyncTriggers(adapter, ftsTable, contentTable, columns) {
|
|
134
|
+
const triggersCreated = [];
|
|
135
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
136
|
+
const newColList = columns.map((c) => `NEW."${c}"`).join(", ");
|
|
137
|
+
const oldColList = columns.map((c) => `OLD."${c}"`).join(", ");
|
|
138
|
+
const insertTriggerName = `${ftsTable}_ai`;
|
|
139
|
+
const insertTrigger = `
|
|
140
|
+
CREATE TRIGGER IF NOT EXISTS "${insertTriggerName}" AFTER INSERT ON "${contentTable}" BEGIN
|
|
141
|
+
INSERT INTO "${ftsTable}"(rowid, ${colList}) VALUES (NEW.rowid, ${newColList});
|
|
142
|
+
END;
|
|
143
|
+
`;
|
|
144
|
+
await adapter.executeQuery(insertTrigger);
|
|
145
|
+
triggersCreated.push(insertTriggerName);
|
|
146
|
+
const deleteTriggerName = `${ftsTable}_ad`;
|
|
147
|
+
const deleteTrigger = `
|
|
148
|
+
CREATE TRIGGER IF NOT EXISTS "${deleteTriggerName}" AFTER DELETE ON "${contentTable}" BEGIN
|
|
149
|
+
INSERT INTO "${ftsTable}"("${ftsTable}", rowid, ${colList}) VALUES('delete', OLD.rowid, ${oldColList});
|
|
150
|
+
END;
|
|
151
|
+
`;
|
|
152
|
+
await adapter.executeQuery(deleteTrigger);
|
|
153
|
+
triggersCreated.push(deleteTriggerName);
|
|
154
|
+
const updateTriggerName = `${ftsTable}_au`;
|
|
155
|
+
const updateTrigger = `
|
|
156
|
+
CREATE TRIGGER IF NOT EXISTS "${updateTriggerName}" AFTER UPDATE ON "${contentTable}" BEGIN
|
|
157
|
+
INSERT INTO "${ftsTable}"("${ftsTable}", rowid, ${colList}) VALUES('delete', OLD.rowid, ${oldColList});
|
|
158
|
+
INSERT INTO "${ftsTable}"(rowid, ${colList}) VALUES (NEW.rowid, ${newColList});
|
|
159
|
+
END;
|
|
160
|
+
`;
|
|
161
|
+
await adapter.executeQuery(updateTrigger);
|
|
162
|
+
triggersCreated.push(updateTriggerName);
|
|
163
|
+
return triggersCreated;
|
|
164
|
+
}
|
|
165
|
+
function createFtsSearchTool(adapter) {
|
|
166
|
+
return {
|
|
167
|
+
name: "sqlite_fts_search",
|
|
168
|
+
description: "Search an FTS5 table using full-text query syntax.",
|
|
169
|
+
group: "text",
|
|
170
|
+
inputSchema: FtsSearchSchema,
|
|
171
|
+
outputSchema: FtsSearchOutputSchema,
|
|
172
|
+
requiredScopes: ["read"],
|
|
173
|
+
annotations: readOnly("FTS Search"),
|
|
174
|
+
handler: async (params, _context) => {
|
|
175
|
+
try {
|
|
176
|
+
const input = FtsSearchSchema.parse(params);
|
|
177
|
+
if (!isFts5Available(adapter)) {
|
|
178
|
+
return buildFts5SearchUnavailableError();
|
|
179
|
+
}
|
|
180
|
+
sanitizeIdentifier(input.table);
|
|
181
|
+
await validateTableExists(adapter, input.table);
|
|
182
|
+
let selectClause = "*";
|
|
183
|
+
if (input.highlight) {
|
|
184
|
+
selectClause = `*, highlight("${input.table}", 0, '<b>', '</b>') as snippet`;
|
|
185
|
+
}
|
|
186
|
+
if (input.query === "*" || input.query.trim() === "") {
|
|
187
|
+
const sql2 = `SELECT ${selectClause}, NULL as rank FROM "${input.table}" ORDER BY rowid LIMIT ${input.limit}`;
|
|
188
|
+
const result2 = await adapter.executeReadQuery(sql2);
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
rowCount: result2.rows?.length ?? 0,
|
|
192
|
+
results: result2.rows
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const queryEscaped = input.query.replace(/'/g, "''");
|
|
196
|
+
let matchExpr = `"${input.table}" MATCH '${queryEscaped}'`;
|
|
197
|
+
if (input.columns && input.columns.length > 0) {
|
|
198
|
+
for (const col of input.columns) {
|
|
199
|
+
sanitizeIdentifier(col);
|
|
200
|
+
}
|
|
201
|
+
const colFilter = input.columns.map((c) => `${c}:${queryEscaped}`).join(" OR ");
|
|
202
|
+
matchExpr = `"${input.table}" MATCH '${colFilter}'`;
|
|
203
|
+
}
|
|
204
|
+
const sql = `SELECT ${selectClause}, rank FROM "${input.table}" WHERE ${matchExpr} ORDER BY rank LIMIT ${input.limit}`;
|
|
205
|
+
const result = await adapter.executeReadQuery(sql);
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
rowCount: result.rows?.length ?? 0,
|
|
209
|
+
results: result.rows
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (isFts5UnavailableError(error)) {
|
|
213
|
+
return buildFts5SearchUnavailableError();
|
|
214
|
+
}
|
|
215
|
+
return formatHandlerError(error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function createFtsRebuildTool(adapter) {
|
|
221
|
+
return {
|
|
222
|
+
name: "sqlite_fts_rebuild",
|
|
223
|
+
description: "Rebuild an FTS5 index to optimize search performance.",
|
|
224
|
+
group: "text",
|
|
225
|
+
inputSchema: FtsRebuildSchema,
|
|
226
|
+
outputSchema: FtsRebuildOutputSchema,
|
|
227
|
+
requiredScopes: ["admin"],
|
|
228
|
+
annotations: admin("FTS Rebuild"),
|
|
229
|
+
handler: async (params, _context) => {
|
|
230
|
+
try {
|
|
231
|
+
const input = FtsRebuildSchema.parse(params);
|
|
232
|
+
if (!isFts5Available(adapter)) {
|
|
233
|
+
return buildFts5UnavailableError(input.table);
|
|
234
|
+
}
|
|
235
|
+
sanitizeIdentifier(input.table);
|
|
236
|
+
await validateTableExists(adapter, input.table);
|
|
237
|
+
const sql = `INSERT INTO "${input.table}"("${input.table}") VALUES('rebuild')`;
|
|
238
|
+
await adapter.executeQuery(sql);
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
message: `FTS5 index '${input.table}' rebuilt`,
|
|
242
|
+
tableName: input.table
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (isFts5UnavailableError(error)) {
|
|
246
|
+
return buildFts5UnavailableError(
|
|
247
|
+
params?.table
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return formatHandlerError(error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function createFtsMatchInfoTool(adapter) {
|
|
256
|
+
return {
|
|
257
|
+
name: "sqlite_fts_match_info",
|
|
258
|
+
description: "Get FTS5 match ranking information using bm25.",
|
|
259
|
+
group: "text",
|
|
260
|
+
inputSchema: FtsMatchInfoSchema,
|
|
261
|
+
outputSchema: FtsSearchOutputSchema,
|
|
262
|
+
requiredScopes: ["read"],
|
|
263
|
+
annotations: readOnly("FTS Match Info"),
|
|
264
|
+
handler: async (params, _context) => {
|
|
265
|
+
try {
|
|
266
|
+
const input = FtsMatchInfoSchema.parse(params);
|
|
267
|
+
if (!isFts5Available(adapter)) {
|
|
268
|
+
return buildFts5SearchUnavailableError();
|
|
269
|
+
}
|
|
270
|
+
sanitizeIdentifier(input.table);
|
|
271
|
+
await validateTableExists(adapter, input.table);
|
|
272
|
+
const queryEscaped = input.query.replace(/'/g, "''");
|
|
273
|
+
let rankExpr;
|
|
274
|
+
if (input.format === "bm25") {
|
|
275
|
+
rankExpr = `bm25("${input.table}")`;
|
|
276
|
+
} else {
|
|
277
|
+
rankExpr = "rank";
|
|
278
|
+
}
|
|
279
|
+
const sql = `SELECT *, ${rankExpr} as score FROM "${input.table}" WHERE "${input.table}" MATCH '${queryEscaped}' ORDER BY score`;
|
|
280
|
+
const result = await adapter.executeReadQuery(sql);
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
rowCount: result.rows?.length ?? 0,
|
|
284
|
+
results: result.rows
|
|
285
|
+
};
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (isFts5UnavailableError(error)) {
|
|
288
|
+
return buildFts5SearchUnavailableError();
|
|
289
|
+
}
|
|
290
|
+
return formatHandlerError(error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
var VALID_MODES = ["deferred", "immediate", "exclusive"];
|
|
296
|
+
var BeginTransactionSchema = z.object({
|
|
297
|
+
mode: z.preprocess(
|
|
298
|
+
(val) => {
|
|
299
|
+
if (typeof val !== "string") return void 0;
|
|
300
|
+
const normalized = val.toLowerCase();
|
|
301
|
+
return VALID_MODES.includes(normalized) ? normalized : void 0;
|
|
302
|
+
},
|
|
303
|
+
z.enum(["deferred", "immediate", "exclusive"]).optional().default("deferred").describe(
|
|
304
|
+
"Transaction mode: deferred waits for first write, immediate acquires lock immediately, exclusive blocks all access"
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
});
|
|
308
|
+
var SavepointSchema = z.object({
|
|
309
|
+
name: z.string().describe("Savepoint name")
|
|
310
|
+
});
|
|
311
|
+
var ExecuteInTransactionSchema = z.object({
|
|
312
|
+
statements: z.array(z.string()).describe("Array of SQL statements to execute in order"),
|
|
313
|
+
rollbackOnError: z.boolean().optional().default(true).describe("If true, rollback all changes when any statement fails")
|
|
314
|
+
});
|
|
315
|
+
function getTransactionTools(adapter) {
|
|
316
|
+
return [
|
|
317
|
+
createBeginTransactionTool(adapter),
|
|
318
|
+
createCommitTransactionTool(adapter),
|
|
319
|
+
createRollbackTransactionTool(adapter),
|
|
320
|
+
createSavepointTool(adapter),
|
|
321
|
+
createReleaseSavepointTool(adapter),
|
|
322
|
+
createRollbackToSavepointTool(adapter),
|
|
323
|
+
createExecuteInTransactionTool(adapter)
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
function createBeginTransactionTool(adapter) {
|
|
327
|
+
return {
|
|
328
|
+
name: "sqlite_transaction_begin",
|
|
329
|
+
description: "Begin a new transaction. Use immediate or exclusive mode for write-heavy operations.",
|
|
330
|
+
group: "admin",
|
|
331
|
+
inputSchema: BeginTransactionSchema,
|
|
332
|
+
outputSchema: TransactionBeginOutputSchema,
|
|
333
|
+
annotations: write("Begin Transaction"),
|
|
334
|
+
requiredScopes: ["write"],
|
|
335
|
+
handler: async (params, _context) => {
|
|
336
|
+
try {
|
|
337
|
+
const input = BeginTransactionSchema.parse(params);
|
|
338
|
+
const mode = input.mode.toUpperCase();
|
|
339
|
+
await adapter.executeWriteQuery(`BEGIN ${mode} TRANSACTION`);
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
message: `Transaction started (${input.mode} mode)`,
|
|
343
|
+
mode: input.mode
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return formatHandlerError(error);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function createCommitTransactionTool(adapter) {
|
|
352
|
+
return {
|
|
353
|
+
name: "sqlite_transaction_commit",
|
|
354
|
+
description: "Commit the current transaction, making all changes permanent.",
|
|
355
|
+
group: "admin",
|
|
356
|
+
inputSchema: z.object({}),
|
|
357
|
+
outputSchema: TransactionCommitOutputSchema,
|
|
358
|
+
annotations: write("Commit Transaction"),
|
|
359
|
+
requiredScopes: ["write"],
|
|
360
|
+
handler: (_params, _context) => {
|
|
361
|
+
try {
|
|
362
|
+
adapter.commitTransaction();
|
|
363
|
+
return Promise.resolve({
|
|
364
|
+
success: true,
|
|
365
|
+
message: "Transaction committed"
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return Promise.resolve(formatHandlerError(error));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function createRollbackTransactionTool(adapter) {
|
|
374
|
+
return {
|
|
375
|
+
name: "sqlite_transaction_rollback",
|
|
376
|
+
description: "Rollback the current transaction, discarding all changes.",
|
|
377
|
+
group: "admin",
|
|
378
|
+
outputSchema: TransactionRollbackOutputSchema,
|
|
379
|
+
inputSchema: z.object({}),
|
|
380
|
+
annotations: write("Rollback Transaction"),
|
|
381
|
+
requiredScopes: ["write"],
|
|
382
|
+
handler: (_params, _context) => {
|
|
383
|
+
try {
|
|
384
|
+
adapter.rollbackTransaction();
|
|
385
|
+
return Promise.resolve({
|
|
386
|
+
success: true,
|
|
387
|
+
message: "Transaction rolled back"
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return Promise.resolve(formatHandlerError(error));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function createSavepointTool(adapter) {
|
|
396
|
+
return {
|
|
397
|
+
name: "sqlite_transaction_savepoint",
|
|
398
|
+
description: "Create a savepoint within the current transaction for partial rollback.",
|
|
399
|
+
group: "admin",
|
|
400
|
+
outputSchema: TransactionSavepointOutputSchema,
|
|
401
|
+
inputSchema: SavepointSchema,
|
|
402
|
+
annotations: write("Create Savepoint"),
|
|
403
|
+
requiredScopes: ["write"],
|
|
404
|
+
handler: (params, _context) => {
|
|
405
|
+
try {
|
|
406
|
+
const input = SavepointSchema.parse(params);
|
|
407
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.name)) {
|
|
408
|
+
return Promise.resolve(
|
|
409
|
+
formatHandlerError(
|
|
410
|
+
new ValidationError(
|
|
411
|
+
`Invalid savepoint name '${input.name}': must start with a letter or underscore and contain only alphanumeric characters`
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
adapter.savepoint(input.name);
|
|
417
|
+
return Promise.resolve({
|
|
418
|
+
success: true,
|
|
419
|
+
message: `Savepoint '${input.name}' created`,
|
|
420
|
+
savepoint: input.name
|
|
421
|
+
});
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return Promise.resolve(formatHandlerError(error));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function createReleaseSavepointTool(adapter) {
|
|
429
|
+
return {
|
|
430
|
+
name: "sqlite_transaction_release",
|
|
431
|
+
description: "Release a savepoint, keeping the changes made since it was created.",
|
|
432
|
+
group: "admin",
|
|
433
|
+
inputSchema: SavepointSchema,
|
|
434
|
+
outputSchema: TransactionReleaseOutputSchema,
|
|
435
|
+
annotations: write("Release Savepoint"),
|
|
436
|
+
requiredScopes: ["write"],
|
|
437
|
+
handler: (params, _context) => {
|
|
438
|
+
try {
|
|
439
|
+
const input = SavepointSchema.parse(params);
|
|
440
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.name)) {
|
|
441
|
+
return Promise.resolve(
|
|
442
|
+
formatHandlerError(
|
|
443
|
+
new ValidationError(
|
|
444
|
+
`Invalid savepoint name '${input.name}': must start with a letter or underscore and contain only alphanumeric characters`
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
adapter.releaseSavepoint(input.name);
|
|
450
|
+
return Promise.resolve({
|
|
451
|
+
success: true,
|
|
452
|
+
message: `Savepoint '${input.name}' released`,
|
|
453
|
+
savepoint: input.name
|
|
454
|
+
});
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return Promise.resolve(formatHandlerError(error));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function createRollbackToSavepointTool(adapter) {
|
|
462
|
+
return {
|
|
463
|
+
name: "sqlite_transaction_rollback_to",
|
|
464
|
+
description: "Rollback to a savepoint, discarding changes made after it was created.",
|
|
465
|
+
group: "admin",
|
|
466
|
+
inputSchema: SavepointSchema,
|
|
467
|
+
outputSchema: TransactionRollbackToOutputSchema,
|
|
468
|
+
annotations: write("Rollback to Savepoint"),
|
|
469
|
+
requiredScopes: ["write"],
|
|
470
|
+
handler: (params, _context) => {
|
|
471
|
+
try {
|
|
472
|
+
const input = SavepointSchema.parse(params);
|
|
473
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.name)) {
|
|
474
|
+
return Promise.resolve(
|
|
475
|
+
formatHandlerError(
|
|
476
|
+
new ValidationError(
|
|
477
|
+
`Invalid savepoint name '${input.name}': must start with a letter or underscore and contain only alphanumeric characters`
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
adapter.rollbackToSavepoint(input.name);
|
|
483
|
+
return Promise.resolve({
|
|
484
|
+
success: true,
|
|
485
|
+
message: `Rolled back to savepoint '${input.name}'`,
|
|
486
|
+
savepoint: input.name
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return Promise.resolve(formatHandlerError(error));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function createExecuteInTransactionTool(adapter) {
|
|
495
|
+
return {
|
|
496
|
+
name: "sqlite_transaction_execute",
|
|
497
|
+
description: "Execute multiple SQL statements in a single transaction. Automatically commits on success or rolls back on error.",
|
|
498
|
+
group: "admin",
|
|
499
|
+
outputSchema: TransactionExecuteOutputSchema,
|
|
500
|
+
inputSchema: ExecuteInTransactionSchema,
|
|
501
|
+
annotations: write("Execute in Transaction"),
|
|
502
|
+
requiredScopes: ["write"],
|
|
503
|
+
handler: async (params, _context) => {
|
|
504
|
+
let input;
|
|
505
|
+
try {
|
|
506
|
+
input = ExecuteInTransactionSchema.parse(params);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
return {
|
|
509
|
+
...formatHandlerError(error),
|
|
510
|
+
message: "",
|
|
511
|
+
statementsExecuted: 0,
|
|
512
|
+
results: []
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const results = [];
|
|
516
|
+
let success = true;
|
|
517
|
+
try {
|
|
518
|
+
adapter.beginTransaction();
|
|
519
|
+
for (const statement of input.statements) {
|
|
520
|
+
try {
|
|
521
|
+
const isSelect = statement.trim().toUpperCase().startsWith("SELECT");
|
|
522
|
+
if (isSelect) {
|
|
523
|
+
const result = await adapter.executeReadQuery(statement);
|
|
524
|
+
const rowCount = result.rows?.length ?? 0;
|
|
525
|
+
const statementResult = {
|
|
526
|
+
statement: statement.substring(0, 100) + (statement.length > 100 ? "..." : ""),
|
|
527
|
+
rowCount
|
|
528
|
+
};
|
|
529
|
+
if (result.rows) {
|
|
530
|
+
statementResult.rows = result.rows;
|
|
531
|
+
}
|
|
532
|
+
results.push(statementResult);
|
|
533
|
+
} else {
|
|
534
|
+
const result = await adapter.executeWriteQuery(statement);
|
|
535
|
+
results.push({
|
|
536
|
+
statement: statement.substring(0, 100) + (statement.length > 100 ? "..." : ""),
|
|
537
|
+
rowsAffected: result.rowsAffected ?? 0
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
542
|
+
results.push({
|
|
543
|
+
statement: statement.substring(0, 100) + (statement.length > 100 ? "..." : ""),
|
|
544
|
+
error: message
|
|
545
|
+
});
|
|
546
|
+
if (input.rollbackOnError) {
|
|
547
|
+
throw error;
|
|
548
|
+
}
|
|
549
|
+
success = false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
adapter.commitTransaction();
|
|
553
|
+
return {
|
|
554
|
+
success,
|
|
555
|
+
message: success ? "Transaction completed successfully" : "Transaction completed with errors",
|
|
556
|
+
statementsExecuted: input.statements.length,
|
|
557
|
+
results
|
|
558
|
+
};
|
|
559
|
+
} catch (error) {
|
|
560
|
+
let rollbackFailure;
|
|
561
|
+
try {
|
|
562
|
+
adapter.rollbackTransaction();
|
|
563
|
+
} catch (rbError) {
|
|
564
|
+
rollbackFailure = rbError instanceof Error ? rbError.message : String(rbError);
|
|
565
|
+
}
|
|
566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
567
|
+
const formatted = formatHandlerError(error);
|
|
568
|
+
let rollbackMessage = `Transaction rolled back: ${message}`;
|
|
569
|
+
if (rollbackFailure) {
|
|
570
|
+
rollbackMessage += ` (rollback error: ${rollbackFailure})`;
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
...formatted,
|
|
574
|
+
error: formatted.error ?? rollbackMessage,
|
|
575
|
+
message: rollbackMessage,
|
|
576
|
+
statementsExecuted: results.length,
|
|
577
|
+
results
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
var coerceNumber = (val) => typeof val === "string" ? isNaN(Number(val)) ? void 0 : Number(val) : val;
|
|
584
|
+
var coerceEnumValues2 = (allowed) => (val) => typeof val === "string" && allowed.includes(val) ? val : void 0;
|
|
585
|
+
var VALID_DIRECTIONS = ["lag", "lead"];
|
|
586
|
+
var RowNumberSchema = z.object({
|
|
587
|
+
table: z.string().describe("Table name"),
|
|
588
|
+
orderBy: z.string().describe("Column(s) to order by"),
|
|
589
|
+
partitionBy: z.string().optional().describe("Column(s) to partition by"),
|
|
590
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
591
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
592
|
+
limit: z.preprocess(
|
|
593
|
+
coerceNumber,
|
|
594
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
595
|
+
)
|
|
596
|
+
});
|
|
597
|
+
var RankSchema = z.object({
|
|
598
|
+
table: z.string().describe("Table name"),
|
|
599
|
+
orderBy: z.string().describe("Column(s) to order by (determines rank)"),
|
|
600
|
+
partitionBy: z.string().optional().describe("Column(s) to partition by"),
|
|
601
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
602
|
+
rankType: z.preprocess(
|
|
603
|
+
coerceEnumValues2(["rank", "dense_rank", "percent_rank"]),
|
|
604
|
+
z.enum(["rank", "dense_rank", "percent_rank"]).optional().default("rank").describe("Rank function type")
|
|
605
|
+
),
|
|
606
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
607
|
+
limit: z.preprocess(
|
|
608
|
+
coerceNumber,
|
|
609
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
610
|
+
)
|
|
611
|
+
});
|
|
612
|
+
var LagLeadSchema = z.object({
|
|
613
|
+
table: z.string().describe("Table name"),
|
|
614
|
+
column: z.string().describe("Column to get lag/lead value from"),
|
|
615
|
+
orderBy: z.string().describe("Column(s) to order by"),
|
|
616
|
+
direction: z.string().describe("LAG (previous) or LEAD (next) row"),
|
|
617
|
+
offset: z.preprocess(
|
|
618
|
+
coerceNumber,
|
|
619
|
+
z.number().optional().default(1).describe("Number of rows to look back/ahead")
|
|
620
|
+
),
|
|
621
|
+
defaultValue: z.string().optional().describe("Default value if no row exists"),
|
|
622
|
+
partitionBy: z.string().optional().describe("Column(s) to partition by"),
|
|
623
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
624
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
625
|
+
limit: z.preprocess(
|
|
626
|
+
coerceNumber,
|
|
627
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
628
|
+
)
|
|
629
|
+
});
|
|
630
|
+
var RunningTotalSchema = z.object({
|
|
631
|
+
table: z.string().describe("Table name"),
|
|
632
|
+
column: z.string().describe("Column to sum"),
|
|
633
|
+
orderBy: z.string().describe("Column(s) to order by"),
|
|
634
|
+
partitionBy: z.string().optional().describe("Reset running total for each partition"),
|
|
635
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
636
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
637
|
+
limit: z.preprocess(
|
|
638
|
+
coerceNumber,
|
|
639
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
640
|
+
)
|
|
641
|
+
});
|
|
642
|
+
var MovingAverageSchema = z.object({
|
|
643
|
+
table: z.string().describe("Table name"),
|
|
644
|
+
column: z.string().describe("Column to average"),
|
|
645
|
+
orderBy: z.string().describe("Column(s) to order by"),
|
|
646
|
+
windowSize: z.preprocess(
|
|
647
|
+
coerceNumber,
|
|
648
|
+
z.number().optional().describe("Number of rows in the moving window")
|
|
649
|
+
),
|
|
650
|
+
partitionBy: z.string().optional().describe("Column(s) to partition by"),
|
|
651
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
652
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
653
|
+
limit: z.preprocess(
|
|
654
|
+
coerceNumber,
|
|
655
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
656
|
+
)
|
|
657
|
+
});
|
|
658
|
+
var NtileSchema = z.object({
|
|
659
|
+
table: z.string().describe("Table name"),
|
|
660
|
+
orderBy: z.string().describe("Column(s) to order by"),
|
|
661
|
+
buckets: z.preprocess(
|
|
662
|
+
coerceNumber,
|
|
663
|
+
z.number().optional().describe("Number of buckets (e.g., 4 for quartiles)")
|
|
664
|
+
),
|
|
665
|
+
partitionBy: z.string().optional().describe("Column(s) to partition by"),
|
|
666
|
+
selectColumns: z.array(z.string()).optional().describe("Columns to include in result"),
|
|
667
|
+
whereClause: z.string().optional().describe("Optional WHERE clause"),
|
|
668
|
+
limit: z.preprocess(
|
|
669
|
+
coerceNumber,
|
|
670
|
+
z.number().optional().default(100).describe("Maximum rows to return")
|
|
671
|
+
)
|
|
672
|
+
});
|
|
673
|
+
async function validateTableExists2(adapter, table) {
|
|
674
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
|
|
675
|
+
throw new DbMcpError(
|
|
676
|
+
"Invalid table name",
|
|
677
|
+
"NATIVE_WINDOW_INVALID_TABLE",
|
|
678
|
+
"validation" /* VALIDATION */
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const result = await adapter.executeReadQuery(
|
|
682
|
+
`SELECT 1 FROM sqlite_master WHERE type IN ('table', 'view') AND name='${table}'`
|
|
683
|
+
);
|
|
684
|
+
if (!result.rows || result.rows.length === 0) {
|
|
685
|
+
throw new ResourceNotFoundError(
|
|
686
|
+
`Table '${table}' does not exist`,
|
|
687
|
+
"TABLE_NOT_FOUND",
|
|
688
|
+
{
|
|
689
|
+
suggestion: "Table not found. Run sqlite_list_tables to see available tables.",
|
|
690
|
+
resourceType: "table",
|
|
691
|
+
resourceName: table
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function validateColumnInTable(adapter, table, column) {
|
|
697
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(column)) {
|
|
698
|
+
throw new DbMcpError(
|
|
699
|
+
"Invalid column name",
|
|
700
|
+
"NATIVE_WINDOW_INVALID_COLUMN",
|
|
701
|
+
"validation" /* VALIDATION */
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const tableInfo = await adapter.executeReadQuery(
|
|
705
|
+
`SELECT name FROM pragma_table_info('${table}') WHERE name='${column}'`
|
|
706
|
+
);
|
|
707
|
+
if (!tableInfo.rows || tableInfo.rows.length === 0) {
|
|
708
|
+
throw new ResourceNotFoundError(
|
|
709
|
+
`Column '${column}' not found in table '${table}'`,
|
|
710
|
+
"COLUMN_NOT_FOUND",
|
|
711
|
+
{
|
|
712
|
+
suggestion: "Column not found. Use sqlite_describe_table to see available columns.",
|
|
713
|
+
resourceType: "column",
|
|
714
|
+
resourceName: column
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function validateOrderByColumns(adapter, table, orderBy) {
|
|
720
|
+
const parts = orderBy.split(",");
|
|
721
|
+
for (const part of parts) {
|
|
722
|
+
const tokens = part.trim().split(/\s+/);
|
|
723
|
+
const firstToken = tokens[0];
|
|
724
|
+
if (!firstToken) continue;
|
|
725
|
+
const colName = firstToken.replace(/^"|"$/g, "");
|
|
726
|
+
if (/[.()+*/]/.test(colName)) continue;
|
|
727
|
+
if (/^(ASC|DESC)$/i.test(colName)) continue;
|
|
728
|
+
await validateColumnInTable(adapter, table, colName);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function formatColumns(selectColumns) {
|
|
732
|
+
if (selectColumns === void 0 || selectColumns.length === 0) {
|
|
733
|
+
return "*";
|
|
734
|
+
}
|
|
735
|
+
return selectColumns.map((c) => `"${c}"`).join(", ");
|
|
736
|
+
}
|
|
737
|
+
function getWindowTools(adapter) {
|
|
738
|
+
return [
|
|
739
|
+
createRowNumberTool(adapter),
|
|
740
|
+
createRankTool(adapter),
|
|
741
|
+
createLagLeadTool(adapter),
|
|
742
|
+
createRunningTotalTool(adapter),
|
|
743
|
+
createMovingAverageTool(adapter),
|
|
744
|
+
createNtileTool(adapter)
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
function createRowNumberTool(adapter) {
|
|
748
|
+
return {
|
|
749
|
+
name: "sqlite_window_row_number",
|
|
750
|
+
description: "Assign sequential row numbers based on ordering. Useful for pagination and ranking.",
|
|
751
|
+
group: "stats",
|
|
752
|
+
inputSchema: RowNumberSchema,
|
|
753
|
+
outputSchema: WindowRowNumberOutputSchema,
|
|
754
|
+
annotations: readOnly("Window Row Number"),
|
|
755
|
+
requiredScopes: ["read"],
|
|
756
|
+
handler: async (params, _context) => {
|
|
757
|
+
try {
|
|
758
|
+
const input = RowNumberSchema.parse(params);
|
|
759
|
+
await validateTableExists2(adapter, input.table);
|
|
760
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
761
|
+
const columns = formatColumns(input.selectColumns);
|
|
762
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
763
|
+
let sql = `
|
|
764
|
+
SELECT ${columns},
|
|
765
|
+
ROW_NUMBER() OVER (${partition} ORDER BY ${input.orderBy}) as row_number
|
|
766
|
+
FROM "${input.table}"
|
|
767
|
+
`;
|
|
768
|
+
if (input.whereClause) {
|
|
769
|
+
validateWhereClause(input.whereClause);
|
|
770
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
771
|
+
}
|
|
772
|
+
sql += ` LIMIT ${input.limit}`;
|
|
773
|
+
const result = await adapter.executeReadQuery(sql);
|
|
774
|
+
return {
|
|
775
|
+
success: true,
|
|
776
|
+
rowCount: result.rows?.length ?? 0,
|
|
777
|
+
rows: result.rows
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
return formatHandlerError(error);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function createRankTool(adapter) {
|
|
786
|
+
return {
|
|
787
|
+
name: "sqlite_window_rank",
|
|
788
|
+
description: "Calculate rank of rows. RANK leaves gaps after ties, DENSE_RANK does not, PERCENT_RANK gives 0-1 range.",
|
|
789
|
+
group: "stats",
|
|
790
|
+
inputSchema: RankSchema,
|
|
791
|
+
outputSchema: WindowRankOutputSchema,
|
|
792
|
+
annotations: readOnly("Window Rank"),
|
|
793
|
+
requiredScopes: ["read"],
|
|
794
|
+
handler: async (params, _context) => {
|
|
795
|
+
try {
|
|
796
|
+
const input = RankSchema.parse(params);
|
|
797
|
+
await validateTableExists2(adapter, input.table);
|
|
798
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
799
|
+
const columns = formatColumns(input.selectColumns);
|
|
800
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
801
|
+
const rankFunc = input.rankType.toUpperCase();
|
|
802
|
+
let sql = `
|
|
803
|
+
SELECT ${columns},
|
|
804
|
+
${rankFunc}() OVER (${partition} ORDER BY ${input.orderBy}) as ${input.rankType}
|
|
805
|
+
FROM "${input.table}"
|
|
806
|
+
`;
|
|
807
|
+
if (input.whereClause) {
|
|
808
|
+
validateWhereClause(input.whereClause);
|
|
809
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
810
|
+
}
|
|
811
|
+
sql += ` LIMIT ${input.limit}`;
|
|
812
|
+
const result = await adapter.executeReadQuery(sql);
|
|
813
|
+
return {
|
|
814
|
+
success: true,
|
|
815
|
+
rankType: input.rankType,
|
|
816
|
+
rowCount: result.rows?.length ?? 0,
|
|
817
|
+
rows: result.rows
|
|
818
|
+
};
|
|
819
|
+
} catch (error) {
|
|
820
|
+
return formatHandlerError(error);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function createLagLeadTool(adapter) {
|
|
826
|
+
return {
|
|
827
|
+
name: "sqlite_window_lag_lead",
|
|
828
|
+
description: "Access previous (LAG) or next (LEAD) row values. Useful for comparing consecutive rows.",
|
|
829
|
+
group: "stats",
|
|
830
|
+
inputSchema: LagLeadSchema,
|
|
831
|
+
outputSchema: WindowLagLeadOutputSchema,
|
|
832
|
+
annotations: readOnly("Window Lag/Lead"),
|
|
833
|
+
requiredScopes: ["read"],
|
|
834
|
+
handler: async (params, _context) => {
|
|
835
|
+
try {
|
|
836
|
+
const input = LagLeadSchema.parse(params);
|
|
837
|
+
const normalizedDirection = input.direction.toLowerCase();
|
|
838
|
+
if (!VALID_DIRECTIONS.includes(
|
|
839
|
+
normalizedDirection
|
|
840
|
+
)) {
|
|
841
|
+
return {
|
|
842
|
+
success: false,
|
|
843
|
+
error: `Invalid direction '${input.direction}'. Must be one of: ${VALID_DIRECTIONS.join(", ")}`,
|
|
844
|
+
code: "VALIDATION_ERROR",
|
|
845
|
+
category: "validation",
|
|
846
|
+
recoverable: false
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
await validateTableExists2(adapter, input.table);
|
|
850
|
+
await validateColumnInTable(adapter, input.table, input.column);
|
|
851
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
852
|
+
const columns = formatColumns(input.selectColumns);
|
|
853
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
854
|
+
const func = normalizedDirection.toUpperCase();
|
|
855
|
+
const defaultVal = input.defaultValue !== void 0 ? `, ${input.defaultValue}` : "";
|
|
856
|
+
let sql = `
|
|
857
|
+
SELECT ${columns},
|
|
858
|
+
${func}("${input.column}", ${input.offset}${defaultVal}) OVER (${partition} ORDER BY ${input.orderBy}) as ${normalizedDirection}_value
|
|
859
|
+
FROM "${input.table}"
|
|
860
|
+
`;
|
|
861
|
+
if (input.whereClause) {
|
|
862
|
+
validateWhereClause(input.whereClause);
|
|
863
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
864
|
+
}
|
|
865
|
+
sql += ` LIMIT ${input.limit}`;
|
|
866
|
+
const result = await adapter.executeReadQuery(sql);
|
|
867
|
+
return {
|
|
868
|
+
success: true,
|
|
869
|
+
direction: normalizedDirection,
|
|
870
|
+
offset: input.offset,
|
|
871
|
+
rowCount: result.rows?.length ?? 0,
|
|
872
|
+
rows: result.rows
|
|
873
|
+
};
|
|
874
|
+
} catch (error) {
|
|
875
|
+
return formatHandlerError(error);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function createRunningTotalTool(adapter) {
|
|
881
|
+
return {
|
|
882
|
+
name: "sqlite_window_running_total",
|
|
883
|
+
description: "Calculate running (cumulative) total. Useful for balance tracking, cumulative metrics.",
|
|
884
|
+
group: "stats",
|
|
885
|
+
inputSchema: RunningTotalSchema,
|
|
886
|
+
outputSchema: WindowRunningTotalOutputSchema,
|
|
887
|
+
annotations: readOnly("Window Running Total"),
|
|
888
|
+
requiredScopes: ["read"],
|
|
889
|
+
handler: async (params, _context) => {
|
|
890
|
+
try {
|
|
891
|
+
const input = RunningTotalSchema.parse(
|
|
892
|
+
resolveAliases(params, { valueColumn: "column" })
|
|
893
|
+
);
|
|
894
|
+
await validateTableExists2(adapter, input.table);
|
|
895
|
+
await validateColumnInTable(adapter, input.table, input.column);
|
|
896
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
897
|
+
const columns = formatColumns(input.selectColumns);
|
|
898
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
899
|
+
let sql = `
|
|
900
|
+
SELECT ${columns},
|
|
901
|
+
SUM("${input.column}") OVER (${partition} ORDER BY ${input.orderBy} ROWS UNBOUNDED PRECEDING) as running_total
|
|
902
|
+
FROM "${input.table}"
|
|
903
|
+
`;
|
|
904
|
+
if (input.whereClause) {
|
|
905
|
+
validateWhereClause(input.whereClause);
|
|
906
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
907
|
+
}
|
|
908
|
+
sql += ` LIMIT ${input.limit}`;
|
|
909
|
+
const result = await adapter.executeReadQuery(sql);
|
|
910
|
+
return {
|
|
911
|
+
success: true,
|
|
912
|
+
valueColumn: input.column,
|
|
913
|
+
rowCount: result.rows?.length ?? 0,
|
|
914
|
+
rows: result.rows
|
|
915
|
+
};
|
|
916
|
+
} catch (error) {
|
|
917
|
+
return formatHandlerError(error);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function createMovingAverageTool(adapter) {
|
|
923
|
+
return {
|
|
924
|
+
name: "sqlite_window_moving_avg",
|
|
925
|
+
description: "Calculate moving (rolling) average. Useful for smoothing time series data.",
|
|
926
|
+
group: "stats",
|
|
927
|
+
inputSchema: MovingAverageSchema,
|
|
928
|
+
outputSchema: WindowMovingAvgOutputSchema,
|
|
929
|
+
annotations: readOnly("Window Moving Average"),
|
|
930
|
+
requiredScopes: ["read"],
|
|
931
|
+
handler: async (params, _context) => {
|
|
932
|
+
try {
|
|
933
|
+
const input = MovingAverageSchema.parse(
|
|
934
|
+
resolveAliases(params, { valueColumn: "column" })
|
|
935
|
+
);
|
|
936
|
+
if (input.windowSize === void 0) {
|
|
937
|
+
return {
|
|
938
|
+
success: false,
|
|
939
|
+
error: "'windowSize' is required",
|
|
940
|
+
code: "VALIDATION_ERROR",
|
|
941
|
+
category: "validation",
|
|
942
|
+
recoverable: false
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
await validateTableExists2(adapter, input.table);
|
|
946
|
+
await validateColumnInTable(adapter, input.table, input.column);
|
|
947
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
948
|
+
const columns = formatColumns(input.selectColumns);
|
|
949
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
950
|
+
const preceding = input.windowSize - 1;
|
|
951
|
+
let sql = `
|
|
952
|
+
SELECT ${columns},
|
|
953
|
+
AVG("${input.column}") OVER (${partition} ORDER BY ${input.orderBy} ROWS BETWEEN ${preceding} PRECEDING AND CURRENT ROW) as moving_avg
|
|
954
|
+
FROM "${input.table}"
|
|
955
|
+
`;
|
|
956
|
+
if (input.whereClause) {
|
|
957
|
+
validateWhereClause(input.whereClause);
|
|
958
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
959
|
+
}
|
|
960
|
+
sql += ` LIMIT ${input.limit}`;
|
|
961
|
+
const result = await adapter.executeReadQuery(sql);
|
|
962
|
+
return {
|
|
963
|
+
success: true,
|
|
964
|
+
valueColumn: input.column,
|
|
965
|
+
windowSize: input.windowSize,
|
|
966
|
+
rowCount: result.rows?.length ?? 0,
|
|
967
|
+
rows: result.rows
|
|
968
|
+
};
|
|
969
|
+
} catch (error) {
|
|
970
|
+
return formatHandlerError(error);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
function createNtileTool(adapter) {
|
|
976
|
+
return {
|
|
977
|
+
name: "sqlite_window_ntile",
|
|
978
|
+
description: "Divide rows into N buckets. E.g., 4 buckets = quartiles, 10 = deciles, 100 = percentiles.",
|
|
979
|
+
group: "stats",
|
|
980
|
+
inputSchema: NtileSchema,
|
|
981
|
+
outputSchema: WindowNtileOutputSchema,
|
|
982
|
+
annotations: readOnly("Window Ntile"),
|
|
983
|
+
requiredScopes: ["read"],
|
|
984
|
+
handler: async (params, _context) => {
|
|
985
|
+
try {
|
|
986
|
+
const input = NtileSchema.parse(params);
|
|
987
|
+
if (input.buckets === void 0) {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
error: "'buckets' is required",
|
|
991
|
+
code: "VALIDATION_ERROR",
|
|
992
|
+
category: "validation",
|
|
993
|
+
recoverable: false
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
await validateTableExists2(adapter, input.table);
|
|
997
|
+
await validateOrderByColumns(adapter, input.table, input.orderBy);
|
|
998
|
+
const columns = formatColumns(input.selectColumns);
|
|
999
|
+
const partition = input.partitionBy ? `PARTITION BY ${input.partitionBy}` : "";
|
|
1000
|
+
let sql = `
|
|
1001
|
+
SELECT ${columns},
|
|
1002
|
+
NTILE(${input.buckets}) OVER (${partition} ORDER BY ${input.orderBy}) as ntile
|
|
1003
|
+
FROM "${input.table}"
|
|
1004
|
+
`;
|
|
1005
|
+
if (input.whereClause) {
|
|
1006
|
+
validateWhereClause(input.whereClause);
|
|
1007
|
+
sql += ` WHERE ${input.whereClause}`;
|
|
1008
|
+
}
|
|
1009
|
+
sql += ` LIMIT ${input.limit}`;
|
|
1010
|
+
const result = await adapter.executeReadQuery(sql);
|
|
1011
|
+
return {
|
|
1012
|
+
success: true,
|
|
1013
|
+
buckets: input.buckets,
|
|
1014
|
+
rowCount: result.rows?.length ?? 0,
|
|
1015
|
+
rows: result.rows
|
|
1016
|
+
};
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
return formatHandlerError(error);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
var coerceNumber2 = (val) => typeof val === "string" ? isNaN(Number(val)) ? void 0 : Number(val) : val;
|
|
1024
|
+
var coerceBoolean = (val) => typeof val === "string" ? val === "true" ? true : val === "false" ? false : void 0 : val;
|
|
1025
|
+
function createEnumCoercer(validValues) {
|
|
1026
|
+
return (val) => typeof val === "string" && validValues.includes(val) ? val : typeof val === "string" ? void 0 : val;
|
|
1027
|
+
}
|
|
1028
|
+
var VALID_GEOMETRY_TYPES = [
|
|
1029
|
+
"POINT",
|
|
1030
|
+
"LINESTRING",
|
|
1031
|
+
"POLYGON",
|
|
1032
|
+
"MULTIPOINT",
|
|
1033
|
+
"MULTILINESTRING",
|
|
1034
|
+
"MULTIPOLYGON",
|
|
1035
|
+
"GEOMETRY"
|
|
1036
|
+
];
|
|
1037
|
+
var VALID_ANALYSIS_TYPES = [
|
|
1038
|
+
"nearest_neighbor",
|
|
1039
|
+
"point_in_polygon",
|
|
1040
|
+
"distance_matrix",
|
|
1041
|
+
"spatial_extent"
|
|
1042
|
+
];
|
|
1043
|
+
var VALID_INDEX_ACTIONS = ["create", "drop", "check"];
|
|
1044
|
+
var VALID_FORMATS = ["wkt", "geojson"];
|
|
1045
|
+
var VALID_OPERATIONS = [
|
|
1046
|
+
"buffer",
|
|
1047
|
+
"intersection",
|
|
1048
|
+
"union",
|
|
1049
|
+
"difference",
|
|
1050
|
+
"centroid",
|
|
1051
|
+
"envelope",
|
|
1052
|
+
"simplify"
|
|
1053
|
+
];
|
|
1054
|
+
var coerceGeometryType = createEnumCoercer(VALID_GEOMETRY_TYPES);
|
|
1055
|
+
var LoadSpatialiteSchema = z.object({
|
|
1056
|
+
extensionPath: z.string().optional().describe("Custom path to mod_spatialite extension"),
|
|
1057
|
+
forceReload: z.preprocess(
|
|
1058
|
+
coerceBoolean,
|
|
1059
|
+
z.boolean().optional().default(false).describe("Force reload if already loaded")
|
|
1060
|
+
)
|
|
1061
|
+
});
|
|
1062
|
+
var CreateSpatialTableSchema = z.object({
|
|
1063
|
+
tableName: z.string().describe("Name of the spatial table to create"),
|
|
1064
|
+
geometryColumn: z.string().optional().default("geom").describe("Name of the geometry column"),
|
|
1065
|
+
geometryType: z.preprocess(
|
|
1066
|
+
coerceGeometryType,
|
|
1067
|
+
z.enum([
|
|
1068
|
+
"POINT",
|
|
1069
|
+
"LINESTRING",
|
|
1070
|
+
"POLYGON",
|
|
1071
|
+
"MULTIPOINT",
|
|
1072
|
+
"MULTILINESTRING",
|
|
1073
|
+
"MULTIPOLYGON",
|
|
1074
|
+
"GEOMETRY"
|
|
1075
|
+
]).optional().default("POINT").describe("Type of geometry to store")
|
|
1076
|
+
),
|
|
1077
|
+
srid: z.preprocess(
|
|
1078
|
+
coerceNumber2,
|
|
1079
|
+
z.number().optional().default(4326).describe("Spatial Reference System ID (4326 for WGS84)")
|
|
1080
|
+
),
|
|
1081
|
+
additionalColumns: z.array(
|
|
1082
|
+
z.object({
|
|
1083
|
+
name: z.string(),
|
|
1084
|
+
type: z.string()
|
|
1085
|
+
})
|
|
1086
|
+
).optional().default([]).describe("Additional non-spatial columns")
|
|
1087
|
+
});
|
|
1088
|
+
var SpatialQuerySchema = z.object({
|
|
1089
|
+
query: z.string().describe("Spatial SQL query using SpatiaLite functions"),
|
|
1090
|
+
params: z.array(z.unknown()).optional().default([]).describe("Query parameters")
|
|
1091
|
+
});
|
|
1092
|
+
var SpatialAnalysisSchema = z.object({
|
|
1093
|
+
analysisType: z.string().describe(
|
|
1094
|
+
"Type of spatial analysis: nearest_neighbor, point_in_polygon, distance_matrix, spatial_extent"
|
|
1095
|
+
),
|
|
1096
|
+
sourceTable: z.string().describe("Source table for analysis"),
|
|
1097
|
+
targetTable: z.string().optional().describe(
|
|
1098
|
+
"Target table for operations. For point_in_polygon, this should contain POLYGON geometries while sourceTable contains POINTs"
|
|
1099
|
+
),
|
|
1100
|
+
geometryColumn: z.string().optional().default("geom").describe("Geometry column name"),
|
|
1101
|
+
limit: z.preprocess(
|
|
1102
|
+
coerceNumber2,
|
|
1103
|
+
z.number().optional().default(100).describe("Limit results")
|
|
1104
|
+
),
|
|
1105
|
+
excludeSelf: z.preprocess(
|
|
1106
|
+
coerceBoolean,
|
|
1107
|
+
z.boolean().optional().default(true).describe(
|
|
1108
|
+
"For nearest_neighbor: exclude self-matches when source and target tables are the same (default: true)"
|
|
1109
|
+
)
|
|
1110
|
+
),
|
|
1111
|
+
includeGeometry: z.preprocess(
|
|
1112
|
+
coerceBoolean,
|
|
1113
|
+
z.boolean().optional().default(false).describe(
|
|
1114
|
+
"Include full WKT geometry in results (default: false to reduce payload size)"
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
});
|
|
1118
|
+
var SpatialIndexSchema = z.object({
|
|
1119
|
+
tableName: z.string().describe("Name of the spatial table"),
|
|
1120
|
+
geometryColumn: z.string().optional().default("geom").describe("Geometry column name"),
|
|
1121
|
+
action: z.string().optional().default("create").describe("Action to perform: create, drop, check")
|
|
1122
|
+
});
|
|
1123
|
+
var GeometryTransformSchema = z.object({
|
|
1124
|
+
operation: z.string().describe(
|
|
1125
|
+
"Geometry operation: buffer, intersection, union, difference, centroid, envelope, simplify"
|
|
1126
|
+
),
|
|
1127
|
+
geometry1: z.string().describe("First geometry (WKT format)"),
|
|
1128
|
+
geometry2: z.string().optional().describe("Second geometry for binary ops"),
|
|
1129
|
+
distance: z.preprocess(
|
|
1130
|
+
coerceNumber2,
|
|
1131
|
+
z.number().optional().default(1e-3).describe("Distance for buffer or simplify tolerance")
|
|
1132
|
+
),
|
|
1133
|
+
srid: z.preprocess(
|
|
1134
|
+
coerceNumber2,
|
|
1135
|
+
z.number().optional().default(4326).describe("SRID for result")
|
|
1136
|
+
),
|
|
1137
|
+
simplifyTolerance: z.preprocess(
|
|
1138
|
+
coerceNumber2,
|
|
1139
|
+
z.number().optional().describe(
|
|
1140
|
+
"For buffer operation: apply ST_Simplify to reduce vertices in output polygon. Recommended: 0.0001-0.001 for lat/lon"
|
|
1141
|
+
)
|
|
1142
|
+
)
|
|
1143
|
+
});
|
|
1144
|
+
var SpatialImportSchema = z.object({
|
|
1145
|
+
tableName: z.string().describe("Target table name"),
|
|
1146
|
+
format: z.string().describe("Input format: wkt or geojson"),
|
|
1147
|
+
data: z.string().describe("Geometry data (WKT string or GeoJSON)"),
|
|
1148
|
+
srid: z.preprocess(
|
|
1149
|
+
coerceNumber2,
|
|
1150
|
+
z.number().optional().default(4326).describe("SRID of input data")
|
|
1151
|
+
),
|
|
1152
|
+
additionalData: z.record(z.string(), z.unknown()).optional().describe("Additional column values")
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// src/adapters/sqlite-native/tools/spatialite/loader.ts
|
|
1156
|
+
var SPATIALITE_PATHS = [
|
|
1157
|
+
process.env["SPATIALITE_PATH"],
|
|
1158
|
+
"mod_spatialite",
|
|
1159
|
+
"mod_spatialite.dll",
|
|
1160
|
+
"mod_spatialite.so",
|
|
1161
|
+
"/usr/lib/x86_64-linux-gnu/mod_spatialite.so",
|
|
1162
|
+
"/usr/local/lib/mod_spatialite.so",
|
|
1163
|
+
"/usr/local/lib/mod_spatialite.dylib"
|
|
1164
|
+
].filter((p) => Boolean(p));
|
|
1165
|
+
var loadedDatabases = /* @__PURE__ */ new WeakSet();
|
|
1166
|
+
function tryLoadSpatialite(adapter, customPath) {
|
|
1167
|
+
const db = adapter.getDatabase();
|
|
1168
|
+
if (db === null) {
|
|
1169
|
+
return { success: false, error: "Database not connected" };
|
|
1170
|
+
}
|
|
1171
|
+
const paths = customPath ? [customPath, ...SPATIALITE_PATHS] : SPATIALITE_PATHS;
|
|
1172
|
+
const chosenPathForEnv = customPath ?? process.env["SPATIALITE_PATH"];
|
|
1173
|
+
if (process.platform === "win32" && chosenPathForEnv) {
|
|
1174
|
+
const looksLikeFsPath = chosenPathForEnv.includes("/") || chosenPathForEnv.includes("\\");
|
|
1175
|
+
if (looksLikeFsPath) {
|
|
1176
|
+
const extensionDir = chosenPathForEnv.replace(/[/\\][^/\\]+$/, "");
|
|
1177
|
+
const currentPath = process.env["PATH"] ?? "";
|
|
1178
|
+
if (!currentPath.includes(extensionDir)) {
|
|
1179
|
+
process.env["PATH"] = extensionDir + ";" + currentPath;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
for (const candidatePath of paths) {
|
|
1184
|
+
try {
|
|
1185
|
+
db.loadExtension(candidatePath);
|
|
1186
|
+
db.exec("SELECT InitSpatialMetaData(1)");
|
|
1187
|
+
loadedDatabases.add(db);
|
|
1188
|
+
return { success: true, path: candidatePath };
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
success: false,
|
|
1194
|
+
error: "SpatiaLite extension not found. Install mod_spatialite and set SPATIALITE_PATH environment variable."
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
function isSpatialiteLoaded(adapter) {
|
|
1198
|
+
const db = adapter.getDatabase();
|
|
1199
|
+
if (db === null) return false;
|
|
1200
|
+
if (loadedDatabases.has(db)) return true;
|
|
1201
|
+
try {
|
|
1202
|
+
db.exec("SELECT spatialite_version()");
|
|
1203
|
+
db.exec("SELECT InitSpatialMetaData(1)");
|
|
1204
|
+
loadedDatabases.add(db);
|
|
1205
|
+
return true;
|
|
1206
|
+
} catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function ensureSpatialite(adapter) {
|
|
1211
|
+
if (!isSpatialiteLoaded(adapter)) {
|
|
1212
|
+
const result = tryLoadSpatialite(adapter);
|
|
1213
|
+
if (!result.success) {
|
|
1214
|
+
throw new DbMcpError(
|
|
1215
|
+
result.error ?? "Failed to load SpatiaLite",
|
|
1216
|
+
"SPATIALITE_LOAD_FAILED",
|
|
1217
|
+
"connection" /* CONNECTION */
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/adapters/sqlite-native/tools/spatialite/tools.ts
|
|
1224
|
+
function createLoadSpatialiteTool(adapter) {
|
|
1225
|
+
return {
|
|
1226
|
+
name: "sqlite_spatialite_load",
|
|
1227
|
+
description: "Load SpatiaLite extension for geospatial capabilities. Required before using other spatial tools.",
|
|
1228
|
+
group: "geo",
|
|
1229
|
+
inputSchema: LoadSpatialiteSchema,
|
|
1230
|
+
outputSchema: SpatialiteLoadOutputSchema,
|
|
1231
|
+
requiredScopes: ["admin"],
|
|
1232
|
+
annotations: admin("SpatiaLite Load"),
|
|
1233
|
+
handler: (_params, _context) => {
|
|
1234
|
+
try {
|
|
1235
|
+
const input = LoadSpatialiteSchema.parse(_params);
|
|
1236
|
+
if (!input.forceReload && isSpatialiteLoaded(adapter)) {
|
|
1237
|
+
return Promise.resolve({
|
|
1238
|
+
success: true,
|
|
1239
|
+
message: "SpatiaLite already loaded",
|
|
1240
|
+
alreadyLoaded: true
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
const result = tryLoadSpatialite(adapter, input.extensionPath);
|
|
1244
|
+
if (result.success) {
|
|
1245
|
+
return Promise.resolve({
|
|
1246
|
+
success: true,
|
|
1247
|
+
message: "SpatiaLite loaded successfully",
|
|
1248
|
+
extensionPath: result.path
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
return Promise.resolve({
|
|
1252
|
+
success: false,
|
|
1253
|
+
error: result.error,
|
|
1254
|
+
code: "SPATIALITE_LOAD_FAILED",
|
|
1255
|
+
category: "internal",
|
|
1256
|
+
recoverable: false,
|
|
1257
|
+
searchedPaths: SPATIALITE_PATHS
|
|
1258
|
+
});
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
return Promise.resolve(formatHandlerError(error));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function createSpatialTableTool(adapter) {
|
|
1266
|
+
return {
|
|
1267
|
+
name: "sqlite_spatialite_create_table",
|
|
1268
|
+
description: "Create a spatial table with geometry column using SpatiaLite.",
|
|
1269
|
+
group: "geo",
|
|
1270
|
+
inputSchema: CreateSpatialTableSchema,
|
|
1271
|
+
outputSchema: SpatialiteCreateTableOutputSchema,
|
|
1272
|
+
requiredScopes: ["write"],
|
|
1273
|
+
annotations: write("SpatiaLite Create Table"),
|
|
1274
|
+
handler: async (params, _context) => {
|
|
1275
|
+
try {
|
|
1276
|
+
const input = CreateSpatialTableSchema.parse(params);
|
|
1277
|
+
ensureSpatialite(adapter);
|
|
1278
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.tableName)) {
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
error: `Invalid table name: '${input.tableName}'`,
|
|
1282
|
+
code: "VALIDATION_ERROR",
|
|
1283
|
+
category: "validation",
|
|
1284
|
+
recoverable: false
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
const existsCheck = await adapter.executeReadQuery(
|
|
1288
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='${input.tableName}'`
|
|
1289
|
+
);
|
|
1290
|
+
const alreadyExists = existsCheck.rows != null && existsCheck.rows.length > 0;
|
|
1291
|
+
if (alreadyExists) {
|
|
1292
|
+
return {
|
|
1293
|
+
success: true,
|
|
1294
|
+
message: `Spatial table '${input.tableName}' already exists`,
|
|
1295
|
+
alreadyExists: true,
|
|
1296
|
+
tableName: input.tableName
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const columns = ["id INTEGER PRIMARY KEY AUTOINCREMENT"];
|
|
1300
|
+
for (const col of input.additionalColumns) {
|
|
1301
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(col.name)) {
|
|
1302
|
+
return {
|
|
1303
|
+
success: false,
|
|
1304
|
+
error: `Invalid column name: '${col.name}'`,
|
|
1305
|
+
code: "VALIDATION_ERROR",
|
|
1306
|
+
category: "validation",
|
|
1307
|
+
recoverable: false
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
columns.push(`"${col.name}" ${col.type}`);
|
|
1311
|
+
}
|
|
1312
|
+
await adapter.executeWriteQuery(
|
|
1313
|
+
`CREATE TABLE "${input.tableName}" (${columns.join(", ")})`
|
|
1314
|
+
);
|
|
1315
|
+
const addResult = await adapter.executeReadQuery(
|
|
1316
|
+
`SELECT AddGeometryColumn('${input.tableName}', '${input.geometryColumn}', ${input.srid}, '${input.geometryType}', 'XY')`
|
|
1317
|
+
);
|
|
1318
|
+
const verifyResult = await adapter.executeReadQuery(
|
|
1319
|
+
`SELECT name FROM pragma_table_info('${input.tableName}') WHERE name = '${input.geometryColumn}'`
|
|
1320
|
+
);
|
|
1321
|
+
if (!verifyResult.rows || verifyResult.rows.length === 0) {
|
|
1322
|
+
return {
|
|
1323
|
+
success: false,
|
|
1324
|
+
error: `Failed to create geometry column '${input.geometryColumn}'. AddGeometryColumn returned: ${JSON.stringify(addResult.rows)}`,
|
|
1325
|
+
code: "SPATIALITE_CREATE_FAILED",
|
|
1326
|
+
category: "internal",
|
|
1327
|
+
recoverable: false
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
return {
|
|
1331
|
+
success: true,
|
|
1332
|
+
message: `Spatial table '${input.tableName}' created`,
|
|
1333
|
+
tableName: input.tableName,
|
|
1334
|
+
geometryColumn: input.geometryColumn,
|
|
1335
|
+
geometryType: input.geometryType,
|
|
1336
|
+
srid: input.srid
|
|
1337
|
+
};
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
return formatHandlerError(error);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function createSpatialQueryTool(adapter) {
|
|
1345
|
+
return {
|
|
1346
|
+
name: "sqlite_spatialite_query",
|
|
1347
|
+
description: "Execute spatial SQL queries using SpatiaLite functions (ST_Distance, ST_Within, etc.).",
|
|
1348
|
+
group: "geo",
|
|
1349
|
+
inputSchema: SpatialQuerySchema,
|
|
1350
|
+
outputSchema: SpatialiteQueryOutputSchema,
|
|
1351
|
+
requiredScopes: ["read"],
|
|
1352
|
+
annotations: readOnly("SpatiaLite Query"),
|
|
1353
|
+
handler: async (params, _context) => {
|
|
1354
|
+
try {
|
|
1355
|
+
const input = SpatialQuerySchema.parse(params);
|
|
1356
|
+
ensureSpatialite(adapter);
|
|
1357
|
+
const result = await adapter.executeReadQuery(input.query);
|
|
1358
|
+
return {
|
|
1359
|
+
success: true,
|
|
1360
|
+
rowCount: result.rows?.length ?? 0,
|
|
1361
|
+
rows: result.rows
|
|
1362
|
+
};
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1365
|
+
if (msg.includes("does not return data")) {
|
|
1366
|
+
return {
|
|
1367
|
+
success: false,
|
|
1368
|
+
error: "This tool only supports SELECT queries. Use sqlite_write_query for INSERT/UPDATE/DELETE statements.",
|
|
1369
|
+
code: "QUERY_NOT_SELECT",
|
|
1370
|
+
category: "validation",
|
|
1371
|
+
recoverable: false
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
return formatHandlerError(error);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function createSpatialIndexTool(adapter) {
|
|
1380
|
+
return {
|
|
1381
|
+
name: "sqlite_spatialite_index",
|
|
1382
|
+
description: "Create, drop, or check spatial R-Tree index on geometry column.",
|
|
1383
|
+
group: "geo",
|
|
1384
|
+
inputSchema: SpatialIndexSchema,
|
|
1385
|
+
outputSchema: SpatialiteIndexOutputSchema,
|
|
1386
|
+
requiredScopes: ["write"],
|
|
1387
|
+
annotations: write("SpatiaLite Index"),
|
|
1388
|
+
handler: async (params, _context) => {
|
|
1389
|
+
try {
|
|
1390
|
+
const input = SpatialIndexSchema.parse(params);
|
|
1391
|
+
ensureSpatialite(adapter);
|
|
1392
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.tableName)) {
|
|
1393
|
+
return {
|
|
1394
|
+
success: false,
|
|
1395
|
+
error: `Invalid table name: '${input.tableName}'`,
|
|
1396
|
+
code: "VALIDATION_ERROR",
|
|
1397
|
+
category: "validation",
|
|
1398
|
+
recoverable: false
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
if (!VALID_INDEX_ACTIONS.includes(input.action)) {
|
|
1402
|
+
return {
|
|
1403
|
+
success: false,
|
|
1404
|
+
error: `Invalid action: '${input.action}'. Must be one of: ${VALID_INDEX_ACTIONS.join(", ")}`,
|
|
1405
|
+
code: "VALIDATION_ERROR",
|
|
1406
|
+
category: "validation",
|
|
1407
|
+
recoverable: false
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
const tableCheck = await adapter.executeReadQuery(
|
|
1411
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='${input.tableName}'`
|
|
1412
|
+
);
|
|
1413
|
+
if (!tableCheck.rows || tableCheck.rows.length === 0) {
|
|
1414
|
+
return {
|
|
1415
|
+
success: false,
|
|
1416
|
+
error: `Table '${input.tableName}' does not exist`,
|
|
1417
|
+
code: "TABLE_NOT_FOUND",
|
|
1418
|
+
category: "resource",
|
|
1419
|
+
suggestion: "Table not found. Run sqlite_list_tables to see available tables.",
|
|
1420
|
+
recoverable: false
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
const indexExists = async () => {
|
|
1424
|
+
const idxCheck = await adapter.executeReadQuery(
|
|
1425
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='idx_${input.tableName}_${input.geometryColumn}'`
|
|
1426
|
+
);
|
|
1427
|
+
return (idxCheck.rows?.length ?? 0) > 0;
|
|
1428
|
+
};
|
|
1429
|
+
switch (input.action) {
|
|
1430
|
+
case "create": {
|
|
1431
|
+
if (await indexExists()) {
|
|
1432
|
+
return {
|
|
1433
|
+
success: true,
|
|
1434
|
+
message: `Spatial index already exists on ${input.tableName}.${input.geometryColumn}`,
|
|
1435
|
+
alreadyExists: true,
|
|
1436
|
+
action: "create"
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
await adapter.executeReadQuery(
|
|
1440
|
+
`SELECT CreateSpatialIndex('${input.tableName}', '${input.geometryColumn}')`
|
|
1441
|
+
);
|
|
1442
|
+
return {
|
|
1443
|
+
success: true,
|
|
1444
|
+
message: `Spatial index created on ${input.tableName}.${input.geometryColumn}`,
|
|
1445
|
+
action: "create"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
case "drop": {
|
|
1449
|
+
if (!await indexExists()) {
|
|
1450
|
+
return {
|
|
1451
|
+
success: true,
|
|
1452
|
+
message: `No spatial index exists on ${input.tableName}.${input.geometryColumn}`,
|
|
1453
|
+
alreadyDropped: true,
|
|
1454
|
+
action: "drop"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
await adapter.executeReadQuery(
|
|
1458
|
+
`SELECT DisableSpatialIndex('${input.tableName}', '${input.geometryColumn}')`
|
|
1459
|
+
);
|
|
1460
|
+
return {
|
|
1461
|
+
success: true,
|
|
1462
|
+
message: `Spatial index dropped from ${input.tableName}.${input.geometryColumn}`,
|
|
1463
|
+
action: "drop"
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
case "check": {
|
|
1467
|
+
const hasIndex = await indexExists();
|
|
1468
|
+
if (!hasIndex) {
|
|
1469
|
+
return {
|
|
1470
|
+
success: true,
|
|
1471
|
+
message: "No spatial index found",
|
|
1472
|
+
action: "check",
|
|
1473
|
+
indexed: false
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const checkResult = await adapter.executeReadQuery(
|
|
1477
|
+
`SELECT CheckSpatialIndex('${input.tableName}', '${input.geometryColumn}')`
|
|
1478
|
+
);
|
|
1479
|
+
const checkValue = checkResult.rows?.[0];
|
|
1480
|
+
const rawResult = checkValue != null ? Object.values(checkValue)[0] : null;
|
|
1481
|
+
if (rawResult === 1) {
|
|
1482
|
+
return {
|
|
1483
|
+
success: true,
|
|
1484
|
+
message: "Spatial index is valid",
|
|
1485
|
+
action: "check",
|
|
1486
|
+
indexed: true,
|
|
1487
|
+
valid: true
|
|
1488
|
+
};
|
|
1489
|
+
} else if (rawResult === 0) {
|
|
1490
|
+
return {
|
|
1491
|
+
success: true,
|
|
1492
|
+
message: "Spatial index exists but is invalid (rebuild recommended)",
|
|
1493
|
+
action: "check",
|
|
1494
|
+
indexed: true,
|
|
1495
|
+
valid: false
|
|
1496
|
+
};
|
|
1497
|
+
} else {
|
|
1498
|
+
return {
|
|
1499
|
+
success: true,
|
|
1500
|
+
message: "Spatial index exists (validation inconclusive \u2014 common in SpatiaLite 5.x)",
|
|
1501
|
+
action: "check",
|
|
1502
|
+
indexed: true,
|
|
1503
|
+
valid: null
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
default:
|
|
1508
|
+
return {
|
|
1509
|
+
success: false,
|
|
1510
|
+
error: `Invalid action: '${input.action}'. Must be one of: ${VALID_INDEX_ACTIONS.join(", ")}`,
|
|
1511
|
+
code: "VALIDATION_ERROR",
|
|
1512
|
+
category: "validation",
|
|
1513
|
+
recoverable: false
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
return formatHandlerError(error);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/adapters/sqlite-native/tools/spatialite/analysis.ts
|
|
1524
|
+
function createSpatialAnalysisTool(adapter) {
|
|
1525
|
+
return {
|
|
1526
|
+
name: "sqlite_spatialite_analyze",
|
|
1527
|
+
description: "Perform spatial analysis: nearest neighbor, point in polygon, distance matrix. For point_in_polygon, sourceTable should contain POINTs and targetTable should contain POLYGONs.",
|
|
1528
|
+
group: "geo",
|
|
1529
|
+
inputSchema: SpatialAnalysisSchema,
|
|
1530
|
+
outputSchema: SpatialiteAnalyzeOutputSchema,
|
|
1531
|
+
requiredScopes: ["read"],
|
|
1532
|
+
annotations: readOnly("SpatiaLite Analyze"),
|
|
1533
|
+
handler: async (params, _context) => {
|
|
1534
|
+
try {
|
|
1535
|
+
const input = SpatialAnalysisSchema.parse(params);
|
|
1536
|
+
ensureSpatialite(adapter);
|
|
1537
|
+
if (!VALID_ANALYSIS_TYPES.includes(
|
|
1538
|
+
input.analysisType
|
|
1539
|
+
)) {
|
|
1540
|
+
return {
|
|
1541
|
+
success: false,
|
|
1542
|
+
error: `Invalid analysisType: '${input.analysisType}'. Must be one of: ${VALID_ANALYSIS_TYPES.join(", ")}`,
|
|
1543
|
+
code: "VALIDATION_ERROR",
|
|
1544
|
+
category: "validation",
|
|
1545
|
+
recoverable: false
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.sourceTable)) {
|
|
1549
|
+
return {
|
|
1550
|
+
success: false,
|
|
1551
|
+
error: `Invalid source table name: '${input.sourceTable}'`,
|
|
1552
|
+
code: "VALIDATION_ERROR",
|
|
1553
|
+
category: "validation",
|
|
1554
|
+
recoverable: false
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
if (input.targetTable && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.targetTable)) {
|
|
1558
|
+
return {
|
|
1559
|
+
success: false,
|
|
1560
|
+
error: `Invalid target table name: '${input.targetTable}'`,
|
|
1561
|
+
code: "VALIDATION_ERROR",
|
|
1562
|
+
category: "validation",
|
|
1563
|
+
recoverable: false
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
let query;
|
|
1567
|
+
switch (input.analysisType) {
|
|
1568
|
+
case "spatial_extent":
|
|
1569
|
+
query = `SELECT
|
|
1570
|
+
MbrMinX(Extent("${input.geometryColumn}")) as min_x,
|
|
1571
|
+
MbrMinY(Extent("${input.geometryColumn}")) as min_y,
|
|
1572
|
+
MbrMaxX(Extent("${input.geometryColumn}")) as max_x,
|
|
1573
|
+
MbrMaxY(Extent("${input.geometryColumn}")) as max_y,
|
|
1574
|
+
COUNT(*) as feature_count
|
|
1575
|
+
FROM "${input.sourceTable}"`;
|
|
1576
|
+
break;
|
|
1577
|
+
case "nearest_neighbor": {
|
|
1578
|
+
if (!input.targetTable) {
|
|
1579
|
+
return {
|
|
1580
|
+
success: false,
|
|
1581
|
+
error: "Missing required parameter 'targetTable' for nearest neighbor analysis",
|
|
1582
|
+
code: "VALIDATION_ERROR",
|
|
1583
|
+
category: "validation",
|
|
1584
|
+
recoverable: false
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
const sameTable = input.sourceTable === input.targetTable;
|
|
1588
|
+
const selfFilter = sameTable && input.excludeSelf ? "WHERE s.id != t.id" : "";
|
|
1589
|
+
const geomColumns = input.includeGeometry ? `, AsText(s."${input.geometryColumn}") as source_geom, AsText(t."${input.geometryColumn}") as target_geom` : "";
|
|
1590
|
+
query = `SELECT
|
|
1591
|
+
s.id as source_id, t.id as target_id,
|
|
1592
|
+
ST_Distance(s."${input.geometryColumn}", t."${input.geometryColumn}") as distance${geomColumns}
|
|
1593
|
+
FROM "${input.sourceTable}" s, "${input.targetTable}" t
|
|
1594
|
+
${selfFilter}
|
|
1595
|
+
ORDER BY distance LIMIT ${input.limit}`;
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1598
|
+
case "point_in_polygon": {
|
|
1599
|
+
if (!input.targetTable) {
|
|
1600
|
+
return {
|
|
1601
|
+
success: false,
|
|
1602
|
+
error: "Missing required parameter 'targetTable' for point in polygon analysis",
|
|
1603
|
+
code: "VALIDATION_ERROR",
|
|
1604
|
+
category: "validation",
|
|
1605
|
+
recoverable: false
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
const geomCols = input.includeGeometry ? `, AsText(s."${input.geometryColumn}") as source_geom, AsText(t."${input.geometryColumn}") as target_geom` : "";
|
|
1609
|
+
query = `SELECT
|
|
1610
|
+
s.id as source_id, t.id as target_id${geomCols}
|
|
1611
|
+
FROM "${input.sourceTable}" s, "${input.targetTable}" t
|
|
1612
|
+
WHERE ST_Within(s."${input.geometryColumn}", t."${input.geometryColumn}")
|
|
1613
|
+
LIMIT ${input.limit}`;
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
case "distance_matrix": {
|
|
1617
|
+
const dmTarget = input.targetTable ?? input.sourceTable;
|
|
1618
|
+
const dmSameTable = dmTarget === input.sourceTable;
|
|
1619
|
+
const dmFilter = dmSameTable ? "WHERE a.id < b.id" : "";
|
|
1620
|
+
query = `SELECT a.id as id1, b.id as id2,
|
|
1621
|
+
ST_Distance(a."${input.geometryColumn}", b."${input.geometryColumn}") as distance
|
|
1622
|
+
FROM "${input.sourceTable}" a, "${dmTarget}" b
|
|
1623
|
+
${dmFilter}
|
|
1624
|
+
ORDER BY distance LIMIT ${input.limit}`;
|
|
1625
|
+
break;
|
|
1626
|
+
}
|
|
1627
|
+
default:
|
|
1628
|
+
query = "SELECT 1";
|
|
1629
|
+
}
|
|
1630
|
+
const result = await adapter.executeReadQuery(query);
|
|
1631
|
+
return {
|
|
1632
|
+
success: true,
|
|
1633
|
+
analysisType: input.analysisType,
|
|
1634
|
+
rowCount: result.rows?.length ?? 0,
|
|
1635
|
+
results: result.rows
|
|
1636
|
+
};
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
return formatHandlerError(error);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function createGeometryTransformTool(adapter) {
|
|
1644
|
+
return {
|
|
1645
|
+
name: "sqlite_spatialite_transform",
|
|
1646
|
+
description: "Perform geometry operations: buffer, intersection, union, centroid, simplify.",
|
|
1647
|
+
group: "geo",
|
|
1648
|
+
inputSchema: GeometryTransformSchema,
|
|
1649
|
+
outputSchema: SpatialiteTransformOutputSchema,
|
|
1650
|
+
requiredScopes: ["read"],
|
|
1651
|
+
annotations: readOnly("SpatiaLite Transform"),
|
|
1652
|
+
handler: async (params, _context) => {
|
|
1653
|
+
try {
|
|
1654
|
+
const input = GeometryTransformSchema.parse(params);
|
|
1655
|
+
ensureSpatialite(adapter);
|
|
1656
|
+
if (!VALID_OPERATIONS.includes(input.operation)) {
|
|
1657
|
+
return {
|
|
1658
|
+
success: false,
|
|
1659
|
+
error: `Invalid operation: '${input.operation}'. Must be one of: ${VALID_OPERATIONS.join(", ")}`,
|
|
1660
|
+
code: "VALIDATION_ERROR",
|
|
1661
|
+
category: "validation",
|
|
1662
|
+
recoverable: false
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
let query;
|
|
1666
|
+
switch (input.operation) {
|
|
1667
|
+
case "buffer": {
|
|
1668
|
+
const bufferGeom = `Buffer(GeomFromText('${input.geometry1}', ${input.srid}), ${input.distance})`;
|
|
1669
|
+
const defaultTolerance = Math.max(1e-4, input.distance * 0.01);
|
|
1670
|
+
const tolerance = input.simplifyTolerance ?? defaultTolerance;
|
|
1671
|
+
const finalGeom = tolerance > 0 ? `Simplify(${bufferGeom}, ${tolerance})` : bufferGeom;
|
|
1672
|
+
query = `SELECT AsText(${finalGeom}) as result`;
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
case "intersection":
|
|
1676
|
+
if (!input.geometry2) {
|
|
1677
|
+
return {
|
|
1678
|
+
success: false,
|
|
1679
|
+
error: "Second geometry required for intersection",
|
|
1680
|
+
code: "VALIDATION_ERROR",
|
|
1681
|
+
category: "validation",
|
|
1682
|
+
recoverable: false
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
query = `SELECT AsText(Intersection(
|
|
1686
|
+
GeomFromText('${input.geometry1}', ${input.srid}),
|
|
1687
|
+
GeomFromText('${input.geometry2}', ${input.srid})
|
|
1688
|
+
)) as result`;
|
|
1689
|
+
break;
|
|
1690
|
+
case "union":
|
|
1691
|
+
if (!input.geometry2) {
|
|
1692
|
+
return {
|
|
1693
|
+
success: false,
|
|
1694
|
+
error: "Second geometry required for union",
|
|
1695
|
+
code: "VALIDATION_ERROR",
|
|
1696
|
+
category: "validation",
|
|
1697
|
+
recoverable: false
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
query = `SELECT AsText(GUnion(
|
|
1701
|
+
GeomFromText('${input.geometry1}', ${input.srid}),
|
|
1702
|
+
GeomFromText('${input.geometry2}', ${input.srid})
|
|
1703
|
+
)) as result`;
|
|
1704
|
+
break;
|
|
1705
|
+
case "difference":
|
|
1706
|
+
if (!input.geometry2) {
|
|
1707
|
+
return {
|
|
1708
|
+
success: false,
|
|
1709
|
+
error: "Second geometry required for difference",
|
|
1710
|
+
code: "VALIDATION_ERROR",
|
|
1711
|
+
category: "validation",
|
|
1712
|
+
recoverable: false
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
query = `SELECT AsText(Difference(
|
|
1716
|
+
GeomFromText('${input.geometry1}', ${input.srid}),
|
|
1717
|
+
GeomFromText('${input.geometry2}', ${input.srid})
|
|
1718
|
+
)) as result`;
|
|
1719
|
+
break;
|
|
1720
|
+
case "centroid":
|
|
1721
|
+
query = `SELECT AsText(Centroid(GeomFromText('${input.geometry1}', ${input.srid}))) as result`;
|
|
1722
|
+
break;
|
|
1723
|
+
case "envelope":
|
|
1724
|
+
query = `SELECT AsText(Envelope(GeomFromText('${input.geometry1}', ${input.srid}))) as result`;
|
|
1725
|
+
break;
|
|
1726
|
+
case "simplify":
|
|
1727
|
+
query = `SELECT AsText(Simplify(GeomFromText('${input.geometry1}', ${input.srid}), ${input.distance})) as result`;
|
|
1728
|
+
break;
|
|
1729
|
+
default:
|
|
1730
|
+
query = "SELECT 1";
|
|
1731
|
+
}
|
|
1732
|
+
const result = await adapter.executeReadQuery(query);
|
|
1733
|
+
const wktResult = result.rows?.[0]?.["result"];
|
|
1734
|
+
if (wktResult === null || wktResult === void 0) {
|
|
1735
|
+
return {
|
|
1736
|
+
success: false,
|
|
1737
|
+
error: `Invalid geometry: '${input.geometry1}' could not be parsed as WKT`,
|
|
1738
|
+
code: "VALIDATION_ERROR",
|
|
1739
|
+
category: "validation",
|
|
1740
|
+
recoverable: false
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
success: true,
|
|
1745
|
+
operation: input.operation,
|
|
1746
|
+
result: wktResult
|
|
1747
|
+
};
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
return formatHandlerError(error);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
function createSpatialImportTool(adapter) {
|
|
1755
|
+
return {
|
|
1756
|
+
name: "sqlite_spatialite_import",
|
|
1757
|
+
description: "Import geometry data from WKT or GeoJSON into a spatial table.",
|
|
1758
|
+
group: "geo",
|
|
1759
|
+
inputSchema: SpatialImportSchema,
|
|
1760
|
+
outputSchema: SpatialiteImportOutputSchema,
|
|
1761
|
+
requiredScopes: ["write"],
|
|
1762
|
+
annotations: write("SpatiaLite Import"),
|
|
1763
|
+
handler: async (params, _context) => {
|
|
1764
|
+
try {
|
|
1765
|
+
const input = SpatialImportSchema.parse(params);
|
|
1766
|
+
ensureSpatialite(adapter);
|
|
1767
|
+
if (!VALID_FORMATS.includes(input.format)) {
|
|
1768
|
+
return {
|
|
1769
|
+
success: false,
|
|
1770
|
+
error: `Invalid format: '${input.format}'. Must be one of: ${VALID_FORMATS.join(", ")}`,
|
|
1771
|
+
code: "VALIDATION_ERROR",
|
|
1772
|
+
category: "validation",
|
|
1773
|
+
recoverable: false
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.tableName)) {
|
|
1777
|
+
return {
|
|
1778
|
+
success: false,
|
|
1779
|
+
error: `Invalid table name: '${input.tableName}'`,
|
|
1780
|
+
code: "VALIDATION_ERROR",
|
|
1781
|
+
category: "validation",
|
|
1782
|
+
recoverable: false
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
let wkt;
|
|
1786
|
+
if (input.format === "geojson") {
|
|
1787
|
+
try {
|
|
1788
|
+
JSON.parse(input.data);
|
|
1789
|
+
const columns2 = ["geom"];
|
|
1790
|
+
const values2 = [
|
|
1791
|
+
`SetSRID(GeomFromGeoJSON('${input.data}'), ${input.srid})`
|
|
1792
|
+
];
|
|
1793
|
+
if (input.additionalData) {
|
|
1794
|
+
for (const [key, value] of Object.entries(input.additionalData)) {
|
|
1795
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
1796
|
+
return {
|
|
1797
|
+
success: false,
|
|
1798
|
+
error: `Invalid column name: '${key}'`,
|
|
1799
|
+
code: "VALIDATION_ERROR",
|
|
1800
|
+
category: "validation",
|
|
1801
|
+
recoverable: false
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
columns2.push(`"${key}"`);
|
|
1805
|
+
values2.push(
|
|
1806
|
+
typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value)
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
const sql2 = `INSERT INTO "${input.tableName}" (${columns2.join(", ")}) VALUES (${values2.join(", ")})`;
|
|
1811
|
+
const insertResult2 = await adapter.executeWriteQuery(sql2);
|
|
1812
|
+
return {
|
|
1813
|
+
success: true,
|
|
1814
|
+
message: "GeoJSON geometry imported",
|
|
1815
|
+
rowsAffected: insertResult2.rowsAffected ?? 1
|
|
1816
|
+
};
|
|
1817
|
+
} catch (e) {
|
|
1818
|
+
return {
|
|
1819
|
+
success: false,
|
|
1820
|
+
error: `Invalid GeoJSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
1821
|
+
code: "VALIDATION_ERROR",
|
|
1822
|
+
category: "validation",
|
|
1823
|
+
recoverable: false
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
} else {
|
|
1827
|
+
wkt = input.data;
|
|
1828
|
+
}
|
|
1829
|
+
const wktCheck = await adapter.executeReadQuery(
|
|
1830
|
+
`SELECT GeomFromText('${wkt}', ${input.srid}) as geom`
|
|
1831
|
+
);
|
|
1832
|
+
const parsedGeom = wktCheck.rows?.[0]?.["geom"];
|
|
1833
|
+
if (parsedGeom === null || parsedGeom === void 0) {
|
|
1834
|
+
return {
|
|
1835
|
+
success: false,
|
|
1836
|
+
error: `Invalid WKT geometry: '${wkt}' could not be parsed`,
|
|
1837
|
+
code: "VALIDATION_ERROR",
|
|
1838
|
+
category: "validation",
|
|
1839
|
+
recoverable: false
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
const columns = ["geom"];
|
|
1843
|
+
const values = [`GeomFromText('${wkt}', ${input.srid})`];
|
|
1844
|
+
if (input.additionalData) {
|
|
1845
|
+
for (const [key, value] of Object.entries(input.additionalData)) {
|
|
1846
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
1847
|
+
return {
|
|
1848
|
+
success: false,
|
|
1849
|
+
error: `Invalid column name: '${key}'`,
|
|
1850
|
+
code: "VALIDATION_ERROR",
|
|
1851
|
+
category: "validation",
|
|
1852
|
+
recoverable: false
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
columns.push(`"${key}"`);
|
|
1856
|
+
values.push(
|
|
1857
|
+
typeof value === "string" ? `'${value.replace(/'/g, "''")}'` : String(value)
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
const sql = `INSERT INTO "${input.tableName}" (${columns.join(", ")}) VALUES (${values.join(", ")})`;
|
|
1862
|
+
const insertResult = await adapter.executeWriteQuery(sql);
|
|
1863
|
+
return {
|
|
1864
|
+
success: true,
|
|
1865
|
+
message: "WKT geometry imported",
|
|
1866
|
+
rowsAffected: insertResult.rowsAffected ?? 1
|
|
1867
|
+
};
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
return formatHandlerError(error);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// src/adapters/sqlite-native/tools/spatialite/index.ts
|
|
1876
|
+
function getSpatialiteTools(adapter) {
|
|
1877
|
+
return [
|
|
1878
|
+
createLoadSpatialiteTool(adapter),
|
|
1879
|
+
createSpatialTableTool(adapter),
|
|
1880
|
+
createSpatialQueryTool(adapter),
|
|
1881
|
+
createSpatialAnalysisTool(adapter),
|
|
1882
|
+
createSpatialIndexTool(adapter),
|
|
1883
|
+
createGeometryTransformTool(adapter),
|
|
1884
|
+
createSpatialImportTool(adapter)
|
|
1885
|
+
];
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/adapters/sqlite-native/registration/index.ts
|
|
1889
|
+
function getNativeToolDefinitions(adapter) {
|
|
1890
|
+
const sharedAdapter = adapter;
|
|
1891
|
+
const tools = [
|
|
1892
|
+
...getCoreTools(sharedAdapter),
|
|
1893
|
+
...getJsonOperationTools(sharedAdapter),
|
|
1894
|
+
...getJsonHelperTools(sharedAdapter),
|
|
1895
|
+
...getTextTools(sharedAdapter),
|
|
1896
|
+
...getFtsTools(sharedAdapter),
|
|
1897
|
+
...getStatsTools(sharedAdapter),
|
|
1898
|
+
...getVirtualTools(sharedAdapter),
|
|
1899
|
+
...getVectorTools(sharedAdapter),
|
|
1900
|
+
...getGeoTools(sharedAdapter),
|
|
1901
|
+
...getAdminTools(sharedAdapter),
|
|
1902
|
+
...getIntrospectionTools(sharedAdapter),
|
|
1903
|
+
...getMigrationTools(sharedAdapter),
|
|
1904
|
+
...getCodeModeTools(sharedAdapter),
|
|
1905
|
+
// Native-only tools
|
|
1906
|
+
...getTransactionTools(adapter),
|
|
1907
|
+
...getWindowTools(adapter),
|
|
1908
|
+
...getSpatialiteTools(adapter)
|
|
1909
|
+
];
|
|
1910
|
+
for (const tool of tools) {
|
|
1911
|
+
if (!tool.icons) {
|
|
1912
|
+
const icons = getToolGroupIcon(tool.group);
|
|
1913
|
+
if (icons) {
|
|
1914
|
+
tool.icons = icons;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return tools;
|
|
1919
|
+
}
|
|
1920
|
+
function findProjectRoot(startDir) {
|
|
1921
|
+
let dir = startDir;
|
|
1922
|
+
for (let i = 0; i < 10; i++) {
|
|
1923
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
1924
|
+
const parent = path.dirname(dir);
|
|
1925
|
+
if (parent === dir) break;
|
|
1926
|
+
dir = parent;
|
|
1927
|
+
}
|
|
1928
|
+
return startDir;
|
|
1929
|
+
}
|
|
1930
|
+
var __moduleFilename = fileURLToPath(import.meta.url);
|
|
1931
|
+
var PROJECT_ROOT = findProjectRoot(path.dirname(__moduleFilename));
|
|
1932
|
+
var EXTENSIONS_DIR = path.join(PROJECT_ROOT, "extensions");
|
|
1933
|
+
function tryLoadExtension(db, name, envVar, candidates, log2) {
|
|
1934
|
+
for (const extPath of candidates) {
|
|
1935
|
+
try {
|
|
1936
|
+
db.loadExtension(extPath);
|
|
1937
|
+
log2.info(`Loaded ${name} extension from ${extPath}`, {
|
|
1938
|
+
code: "SQLITE_EXTENSION"
|
|
1939
|
+
});
|
|
1940
|
+
return;
|
|
1941
|
+
} catch {
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
log2.warning(`${name} extension not available. Set ${envVar} env var.`, {
|
|
1945
|
+
code: "SQLITE_EXTENSION"
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
function loadSpatialite(db, log2) {
|
|
1949
|
+
const spatialitePaths = [
|
|
1950
|
+
process.env["SPATIALITE_PATH"],
|
|
1951
|
+
// Absolute paths to local extensions
|
|
1952
|
+
path.join(
|
|
1953
|
+
EXTENSIONS_DIR,
|
|
1954
|
+
"mod_spatialite-5.1.0-win-amd64",
|
|
1955
|
+
"mod_spatialite"
|
|
1956
|
+
),
|
|
1957
|
+
path.join(
|
|
1958
|
+
EXTENSIONS_DIR,
|
|
1959
|
+
"mod_spatialite-5.1.0-win-amd64",
|
|
1960
|
+
"mod_spatialite.dll"
|
|
1961
|
+
),
|
|
1962
|
+
// System paths
|
|
1963
|
+
"mod_spatialite",
|
|
1964
|
+
"mod_spatialite.dll",
|
|
1965
|
+
"/usr/lib/x86_64-linux-gnu/mod_spatialite.so",
|
|
1966
|
+
"/usr/local/lib/mod_spatialite.so",
|
|
1967
|
+
"/usr/local/lib/mod_spatialite.dylib"
|
|
1968
|
+
].filter((p) => Boolean(p));
|
|
1969
|
+
const envSpatialitePath = process.env["SPATIALITE_PATH"];
|
|
1970
|
+
if (envSpatialitePath && process.platform === "win32") {
|
|
1971
|
+
const spatialiteExtDir = path.dirname(envSpatialitePath);
|
|
1972
|
+
const currentPath = process.env["PATH"] ?? "";
|
|
1973
|
+
if (!currentPath.includes(spatialiteExtDir)) {
|
|
1974
|
+
process.env["PATH"] = spatialiteExtDir + ";" + currentPath;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
tryLoadExtension(db, "SpatiaLite", "SPATIALITE_PATH", spatialitePaths, log2);
|
|
1978
|
+
}
|
|
1979
|
+
function loadCsvExtension(db, log2) {
|
|
1980
|
+
const csvPaths = [
|
|
1981
|
+
process.env["CSV_EXTENSION_PATH"],
|
|
1982
|
+
// sqlite-xsv extension with absolute paths
|
|
1983
|
+
path.join(EXTENSIONS_DIR, "xsv0.dll"),
|
|
1984
|
+
path.join(EXTENSIONS_DIR, "xsv0"),
|
|
1985
|
+
// System paths
|
|
1986
|
+
"xsv0",
|
|
1987
|
+
"xsv0.dll",
|
|
1988
|
+
"csv",
|
|
1989
|
+
"csv.dll",
|
|
1990
|
+
"csv.so",
|
|
1991
|
+
"/usr/local/lib/csv.so",
|
|
1992
|
+
"/usr/local/lib/csv.dylib"
|
|
1993
|
+
].filter((p) => Boolean(p));
|
|
1994
|
+
tryLoadExtension(db, "CSV", "CSV_EXTENSION_PATH", csvPaths, log2);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/adapters/sqlite-native/transaction-methods.ts
|
|
1998
|
+
function beginTransaction(db) {
|
|
1999
|
+
db.exec("BEGIN TRANSACTION");
|
|
2000
|
+
}
|
|
2001
|
+
function commitTransaction(db) {
|
|
2002
|
+
db.exec("COMMIT");
|
|
2003
|
+
}
|
|
2004
|
+
function rollbackTransaction(db) {
|
|
2005
|
+
db.exec("ROLLBACK");
|
|
2006
|
+
}
|
|
2007
|
+
function savepoint(db, name) {
|
|
2008
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
2009
|
+
throw new ValidationError(
|
|
2010
|
+
"Invalid savepoint name. Must start with a letter/underscore and contain only alphanumeric chars.",
|
|
2011
|
+
"INVALID_SAVEPOINT_NAME"
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
db.exec(`SAVEPOINT "${name}"`);
|
|
2015
|
+
}
|
|
2016
|
+
function releaseSavepoint(db, name) {
|
|
2017
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
2018
|
+
throw new Error(
|
|
2019
|
+
"Invalid savepoint name. Must start with a letter/underscore and contain only alphanumeric chars."
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
db.exec(`RELEASE SAVEPOINT "${name}"`);
|
|
2023
|
+
}
|
|
2024
|
+
function rollbackToSavepoint(db, name) {
|
|
2025
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
2026
|
+
throw new Error(
|
|
2027
|
+
"Invalid savepoint name. Must start with a letter/underscore and contain only alphanumeric chars."
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
db.exec(`ROLLBACK TO SAVEPOINT "${name}"`);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/adapters/sqlite-native/native-query-executor.ts
|
|
2034
|
+
function nativeExecuteRead(db, sql, params, log2) {
|
|
2035
|
+
const start = Date.now();
|
|
2036
|
+
try {
|
|
2037
|
+
const stmt = db.prepare(sql);
|
|
2038
|
+
const normalizedParams = normalizeSqliteParams(params);
|
|
2039
|
+
const rows = normalizedParams ? stmt.all(...normalizedParams) : stmt.all();
|
|
2040
|
+
return Promise.resolve({
|
|
2041
|
+
rows,
|
|
2042
|
+
columns: stmt.columns().map((c) => ({ name: c.name, type: c.type ?? "unknown" })),
|
|
2043
|
+
executionTimeMs: Date.now() - start
|
|
2044
|
+
});
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2047
|
+
log2.error(`Query failed: ${message}`, {
|
|
2048
|
+
code: ERROR_CODES.DB.QUERY_FAILED.full
|
|
2049
|
+
});
|
|
2050
|
+
throw new QueryError(
|
|
2051
|
+
`Query execution failed: ${message}`,
|
|
2052
|
+
"DB_QUERY_FAILED",
|
|
2053
|
+
{
|
|
2054
|
+
sql,
|
|
2055
|
+
cause: error instanceof Error ? error : void 0
|
|
2056
|
+
}
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function nativeExecuteWrite(db, sql, params, log2) {
|
|
2061
|
+
const start = Date.now();
|
|
2062
|
+
try {
|
|
2063
|
+
const stmt = db.prepare(sql);
|
|
2064
|
+
const normalizedParams = normalizeSqliteParams(params);
|
|
2065
|
+
const info = normalizedParams ? stmt.run(...normalizedParams) : stmt.run();
|
|
2066
|
+
return Promise.resolve({
|
|
2067
|
+
rows: [],
|
|
2068
|
+
rowsAffected: info.changes,
|
|
2069
|
+
executionTimeMs: Date.now() - start
|
|
2070
|
+
});
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2073
|
+
log2.error(`Write query failed: ${message}`, {
|
|
2074
|
+
code: ERROR_CODES.DB.QUERY_FAILED.full
|
|
2075
|
+
});
|
|
2076
|
+
throw new QueryError(`Write query failed: ${message}`, "DB_WRITE_FAILED", {
|
|
2077
|
+
sql,
|
|
2078
|
+
cause: error instanceof Error ? error : void 0
|
|
2079
|
+
});
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/adapters/sqlite-native/native-sqlite-adapter.ts
|
|
2084
|
+
var log = logger.child("NATIVE_SQLITE");
|
|
2085
|
+
var NativeSqliteAdapter = class extends DatabaseAdapter {
|
|
2086
|
+
type = "sqlite";
|
|
2087
|
+
name = "Native SQLite Adapter (better-sqlite3)";
|
|
2088
|
+
version = "1.0.0";
|
|
2089
|
+
/**
|
|
2090
|
+
* Check if this adapter uses native (better-sqlite3) backend.
|
|
2091
|
+
* Returns true for native adapter.
|
|
2092
|
+
*/
|
|
2093
|
+
isNativeBackend() {
|
|
2094
|
+
return true;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Get the configured database file path.
|
|
2098
|
+
* Returns the user-configured path, not internal WASM virtual filesystem paths.
|
|
2099
|
+
*/
|
|
2100
|
+
getConfiguredPath() {
|
|
2101
|
+
const config = this.config;
|
|
2102
|
+
return config?.filePath ?? config?.connectionString ?? ":memory:";
|
|
2103
|
+
}
|
|
2104
|
+
db = null;
|
|
2105
|
+
schemaManager = null;
|
|
2106
|
+
cachedToolDefinitions = null;
|
|
2107
|
+
/**
|
|
2108
|
+
* Connect to a SQLite database using better-sqlite3
|
|
2109
|
+
*/
|
|
2110
|
+
connect(config) {
|
|
2111
|
+
if (config.type !== "sqlite") {
|
|
2112
|
+
throw new ConfigurationError(
|
|
2113
|
+
`Invalid database type: expected 'sqlite', got '${config.type}'`,
|
|
2114
|
+
"DB_TYPE_MISMATCH"
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
const sqliteConfig = config;
|
|
2118
|
+
this.config = sqliteConfig;
|
|
2119
|
+
try {
|
|
2120
|
+
const filePath = sqliteConfig.filePath ?? sqliteConfig.connectionString ?? ":memory:";
|
|
2121
|
+
this.db = new Database(filePath, {
|
|
2122
|
+
readonly: false,
|
|
2123
|
+
fileMustExist: false
|
|
2124
|
+
});
|
|
2125
|
+
log.info(`Connected to SQLite database (native): ${filePath}`, {
|
|
2126
|
+
code: "SQLITE_CONNECT"
|
|
2127
|
+
});
|
|
2128
|
+
this.applyOptions(sqliteConfig.options);
|
|
2129
|
+
const db = this.db;
|
|
2130
|
+
autoEnableWal(
|
|
2131
|
+
{ runPragma: (pragma) => db.pragma(pragma) },
|
|
2132
|
+
filePath,
|
|
2133
|
+
sqliteConfig.options,
|
|
2134
|
+
log
|
|
2135
|
+
);
|
|
2136
|
+
detectAndSetJsonbSupport(() => {
|
|
2137
|
+
const versionResult = db.prepare("SELECT sqlite_version()").get();
|
|
2138
|
+
return versionResult?.["sqlite_version()"] ?? "0.0.0";
|
|
2139
|
+
}, log);
|
|
2140
|
+
this.schemaManager = new SchemaManager(this);
|
|
2141
|
+
this.connected = true;
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2144
|
+
log.error(`Failed to connect to native SQLite: ${message}`, {
|
|
2145
|
+
code: ERROR_CODES.DB.CONNECT_FAILED.full
|
|
2146
|
+
});
|
|
2147
|
+
throw new ConnectionError(
|
|
2148
|
+
`Native SQLite connection failed: ${message}`,
|
|
2149
|
+
"DB_CONNECT_FAILED",
|
|
2150
|
+
{
|
|
2151
|
+
cause: error instanceof Error ? error : void 0
|
|
2152
|
+
}
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
return Promise.resolve();
|
|
2156
|
+
}
|
|
2157
|
+
/**
|
|
2158
|
+
* Apply SQLite PRAGMA options and load extensions
|
|
2159
|
+
*/
|
|
2160
|
+
applyOptions(options) {
|
|
2161
|
+
if (!this.db || !options) return;
|
|
2162
|
+
const db = this.db;
|
|
2163
|
+
applyCommonPragmas({ runPragma: (pragma) => db.pragma(pragma) }, options);
|
|
2164
|
+
if (options.spatialite) {
|
|
2165
|
+
loadSpatialite(db, log);
|
|
2166
|
+
}
|
|
2167
|
+
if (options.csv) {
|
|
2168
|
+
loadCsvExtension(db, log);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Disconnect from the database
|
|
2173
|
+
*/
|
|
2174
|
+
disconnect() {
|
|
2175
|
+
if (this.db) {
|
|
2176
|
+
this.db.close();
|
|
2177
|
+
this.db = null;
|
|
2178
|
+
this.connected = false;
|
|
2179
|
+
log.info("Disconnected from native SQLite database", {
|
|
2180
|
+
code: "SQLITE_DISCONNECT"
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
return Promise.resolve();
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Get health status
|
|
2187
|
+
*/
|
|
2188
|
+
getHealth() {
|
|
2189
|
+
if (!this.db || !this.connected) {
|
|
2190
|
+
return Promise.resolve({
|
|
2191
|
+
connected: false,
|
|
2192
|
+
latencyMs: 0
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
const start = Date.now();
|
|
2196
|
+
try {
|
|
2197
|
+
this.db.prepare("SELECT 1").get();
|
|
2198
|
+
return Promise.resolve({
|
|
2199
|
+
connected: true,
|
|
2200
|
+
latencyMs: Date.now() - start,
|
|
2201
|
+
details: {
|
|
2202
|
+
backend: "better-sqlite3",
|
|
2203
|
+
fts5: this.hasFts5(),
|
|
2204
|
+
spatialite: isSpatialiteLoaded(this)
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
} catch {
|
|
2208
|
+
return Promise.resolve({
|
|
2209
|
+
connected: false,
|
|
2210
|
+
latencyMs: Date.now() - start
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Check if FTS5 is available via compile options.
|
|
2216
|
+
* Uses PRAGMA compile_options instead of creating a virtual table probe,
|
|
2217
|
+
* which can fail when SpatiaLite extensions are loaded.
|
|
2218
|
+
*/
|
|
2219
|
+
hasFts5() {
|
|
2220
|
+
if (!this.db) return false;
|
|
2221
|
+
try {
|
|
2222
|
+
const rows = this.db.prepare("PRAGMA compile_options").all();
|
|
2223
|
+
return rows.some((r) => r.compile_options === "ENABLE_FTS5");
|
|
2224
|
+
} catch {
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
/**
|
|
2229
|
+
* Execute a read query
|
|
2230
|
+
*/
|
|
2231
|
+
executeReadQuery(sql, params) {
|
|
2232
|
+
this.ensureConnected();
|
|
2233
|
+
return nativeExecuteRead(this.ensureDb(), sql, params, log);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Execute a write query
|
|
2237
|
+
*/
|
|
2238
|
+
executeWriteQuery(sql, params) {
|
|
2239
|
+
this.ensureConnected();
|
|
2240
|
+
const result = nativeExecuteWrite(this.ensureDb(), sql, params, log);
|
|
2241
|
+
if (isDDL(sql)) {
|
|
2242
|
+
this.clearSchemaCache();
|
|
2243
|
+
}
|
|
2244
|
+
return result;
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Execute any query
|
|
2248
|
+
*/
|
|
2249
|
+
executeQuery(sql, params) {
|
|
2250
|
+
const trimmed = sql.trim().toUpperCase();
|
|
2251
|
+
if (trimmed.startsWith("SELECT") || trimmed.startsWith("PRAGMA") || trimmed.startsWith("EXPLAIN")) {
|
|
2252
|
+
return this.executeReadQuery(sql, params);
|
|
2253
|
+
}
|
|
2254
|
+
return this.executeWriteQuery(sql, params);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Get schema information (cached via SchemaManager)
|
|
2258
|
+
*/
|
|
2259
|
+
async getSchema() {
|
|
2260
|
+
this.ensureConnected();
|
|
2261
|
+
if (this.schemaManager) {
|
|
2262
|
+
return this.schemaManager.getSchema();
|
|
2263
|
+
}
|
|
2264
|
+
const tables = await this.listTables();
|
|
2265
|
+
return { tables, indexes: [] };
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* List all tables (cached via SchemaManager)
|
|
2269
|
+
*/
|
|
2270
|
+
async listTables() {
|
|
2271
|
+
this.ensureConnected();
|
|
2272
|
+
if (this.schemaManager) {
|
|
2273
|
+
return this.schemaManager.listTables();
|
|
2274
|
+
}
|
|
2275
|
+
const result = await this.executeReadQuery(
|
|
2276
|
+
`SELECT name, type FROM sqlite_master
|
|
2277
|
+
WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'
|
|
2278
|
+
ORDER BY name`
|
|
2279
|
+
);
|
|
2280
|
+
const tables = [];
|
|
2281
|
+
for (const row of result.rows ?? []) {
|
|
2282
|
+
const name = row["name"];
|
|
2283
|
+
const type = row["type"];
|
|
2284
|
+
const tableInfo = await this.describeTable(name);
|
|
2285
|
+
tables.push({ ...tableInfo, type });
|
|
2286
|
+
}
|
|
2287
|
+
return tables;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Describe a table (cached via SchemaManager)
|
|
2291
|
+
*/
|
|
2292
|
+
async describeTable(tableName) {
|
|
2293
|
+
this.ensureConnected();
|
|
2294
|
+
if (this.schemaManager) {
|
|
2295
|
+
return this.schemaManager.describeTable(tableName);
|
|
2296
|
+
}
|
|
2297
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
2298
|
+
throw new DbMcpError(
|
|
2299
|
+
"Invalid table name",
|
|
2300
|
+
"SQLITE_INVALID_TABLE",
|
|
2301
|
+
"validation" /* VALIDATION */
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
const result = await this.executeReadQuery(
|
|
2305
|
+
`PRAGMA table_info("${tableName}")`
|
|
2306
|
+
);
|
|
2307
|
+
if (!result.rows || result.rows.length === 0) {
|
|
2308
|
+
throw new DbMcpError(
|
|
2309
|
+
`Table '${tableName}' does not exist`,
|
|
2310
|
+
"SQLITE_TABLE_NOT_FOUND",
|
|
2311
|
+
"resource" /* RESOURCE */
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
name: tableName,
|
|
2316
|
+
type: "table",
|
|
2317
|
+
columns: (result.rows ?? []).map((c) => ({
|
|
2318
|
+
name: c["name"],
|
|
2319
|
+
type: c["type"],
|
|
2320
|
+
nullable: c["notnull"] === 0,
|
|
2321
|
+
primaryKey: c["pk"] > 0
|
|
2322
|
+
}))
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* List schemas (SQLite only has one)
|
|
2327
|
+
*/
|
|
2328
|
+
listSchemas() {
|
|
2329
|
+
return Promise.resolve(["main"]);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Get all indexes in the database (cached via SchemaManager)
|
|
2333
|
+
* Required by sqlite_indexes resource
|
|
2334
|
+
*/
|
|
2335
|
+
async getAllIndexes() {
|
|
2336
|
+
this.ensureConnected();
|
|
2337
|
+
if (this.schemaManager) {
|
|
2338
|
+
return this.schemaManager.getAllIndexes();
|
|
2339
|
+
}
|
|
2340
|
+
const db = this.ensureDb();
|
|
2341
|
+
const indexes = db.prepare(
|
|
2342
|
+
`SELECT name, tbl_name, sql
|
|
2343
|
+
FROM sqlite_master
|
|
2344
|
+
WHERE type = 'index' AND sql IS NOT NULL
|
|
2345
|
+
ORDER BY tbl_name, name`
|
|
2346
|
+
).all();
|
|
2347
|
+
return indexes.map((idx) => {
|
|
2348
|
+
const indexInfo = db.prepare(`PRAGMA index_info("${idx.name}")`).all();
|
|
2349
|
+
return {
|
|
2350
|
+
name: idx.name,
|
|
2351
|
+
tableName: idx.tbl_name,
|
|
2352
|
+
columns: indexInfo.map((col) => col.name),
|
|
2353
|
+
unique: idx.sql?.toUpperCase().includes("UNIQUE") ?? false
|
|
2354
|
+
};
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Clear the schema metadata cache.
|
|
2359
|
+
* Called automatically after DDL operations.
|
|
2360
|
+
*/
|
|
2361
|
+
clearSchemaCache() {
|
|
2362
|
+
if (this.schemaManager) {
|
|
2363
|
+
this.schemaManager.clearCache();
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Get capabilities
|
|
2368
|
+
*/
|
|
2369
|
+
getCapabilities() {
|
|
2370
|
+
return {
|
|
2371
|
+
json: true,
|
|
2372
|
+
fullTextSearch: true,
|
|
2373
|
+
vector: true,
|
|
2374
|
+
geospatial: true,
|
|
2375
|
+
transactions: true,
|
|
2376
|
+
preparedStatements: true,
|
|
2377
|
+
connectionPooling: false,
|
|
2378
|
+
// better-sqlite3 is single-connection
|
|
2379
|
+
windowFunctions: true,
|
|
2380
|
+
spatialite: true
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
/**
|
|
2384
|
+
* Get supported tool groups
|
|
2385
|
+
*/
|
|
2386
|
+
getSupportedToolGroups() {
|
|
2387
|
+
return [
|
|
2388
|
+
"core",
|
|
2389
|
+
"json",
|
|
2390
|
+
"text",
|
|
2391
|
+
"stats",
|
|
2392
|
+
"vector",
|
|
2393
|
+
"admin",
|
|
2394
|
+
"geo",
|
|
2395
|
+
"introspection",
|
|
2396
|
+
"migration"
|
|
2397
|
+
];
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Get all tool definitions (cached — immutable per adapter instance)
|
|
2401
|
+
*/
|
|
2402
|
+
getToolDefinitions() {
|
|
2403
|
+
if (this.cachedToolDefinitions) return this.cachedToolDefinitions;
|
|
2404
|
+
this.cachedToolDefinitions = getNativeToolDefinitions(this);
|
|
2405
|
+
return this.cachedToolDefinitions;
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Get resource definitions
|
|
2409
|
+
*/
|
|
2410
|
+
getResourceDefinitions() {
|
|
2411
|
+
return getResourceDefinitions(this);
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Get prompt definitions
|
|
2415
|
+
*/
|
|
2416
|
+
getPromptDefinitions() {
|
|
2417
|
+
return getPromptDefinitions(this);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Ensure database is connected
|
|
2421
|
+
*/
|
|
2422
|
+
ensureConnected() {
|
|
2423
|
+
super.ensureConnected();
|
|
2424
|
+
if (!this.db) {
|
|
2425
|
+
throw new ConnectionError(
|
|
2426
|
+
"Not connected to database",
|
|
2427
|
+
"DB_NOT_CONNECTED"
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Ensure database is connected and return database instance
|
|
2433
|
+
*/
|
|
2434
|
+
ensureDb() {
|
|
2435
|
+
this.ensureConnected();
|
|
2436
|
+
const db = this.db;
|
|
2437
|
+
if (!db) {
|
|
2438
|
+
throw new ConnectionError(
|
|
2439
|
+
"Not connected to database",
|
|
2440
|
+
"DB_NOT_CONNECTED"
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return db;
|
|
2444
|
+
}
|
|
2445
|
+
/**
|
|
2446
|
+
* Get the raw database instance (for tools)
|
|
2447
|
+
*/
|
|
2448
|
+
getDatabase() {
|
|
2449
|
+
return this.ensureDb();
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Execute raw SQL and return results (for tools)
|
|
2453
|
+
*/
|
|
2454
|
+
rawQuery(sql, params) {
|
|
2455
|
+
return this.executeQuery(sql, params);
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Begin a transaction
|
|
2459
|
+
*/
|
|
2460
|
+
beginTransaction() {
|
|
2461
|
+
beginTransaction(this.ensureDb());
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* Commit a transaction
|
|
2465
|
+
*/
|
|
2466
|
+
commitTransaction() {
|
|
2467
|
+
commitTransaction(this.ensureDb());
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Rollback a transaction
|
|
2471
|
+
*/
|
|
2472
|
+
rollbackTransaction() {
|
|
2473
|
+
rollbackTransaction(this.ensureDb());
|
|
2474
|
+
}
|
|
2475
|
+
/**
|
|
2476
|
+
* Create a savepoint
|
|
2477
|
+
*/
|
|
2478
|
+
savepoint(name) {
|
|
2479
|
+
savepoint(this.ensureDb(), name);
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Release a savepoint
|
|
2483
|
+
*/
|
|
2484
|
+
releaseSavepoint(name) {
|
|
2485
|
+
releaseSavepoint(this.ensureDb(), name);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Rollback to a savepoint
|
|
2489
|
+
*/
|
|
2490
|
+
rollbackToSavepoint(name) {
|
|
2491
|
+
rollbackToSavepoint(this.ensureDb(), name);
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
function createNativeSqliteAdapter() {
|
|
2495
|
+
return new NativeSqliteAdapter();
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
export { NativeSqliteAdapter, createNativeSqliteAdapter, getSpatialiteTools, getTransactionTools, getWindowTools };
|