@adevguide/mcp-database-server 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1050 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2406 -0
- package/dist/index.js.map +1 -0
- package/mcp-database-server.config.example +59 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2406 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import dotenv from "dotenv";
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { dirname, resolve, join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/types.ts
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
var ConnectionPoolSchema = z.object({
|
|
18
|
+
min: z.number().min(0).optional(),
|
|
19
|
+
max: z.number().min(1).optional(),
|
|
20
|
+
idleTimeoutMillis: z.number().min(0).optional(),
|
|
21
|
+
connectionTimeoutMillis: z.number().min(0).optional()
|
|
22
|
+
});
|
|
23
|
+
var IntrospectionOptionsSchema = z.object({
|
|
24
|
+
includeViews: z.boolean().optional().default(true),
|
|
25
|
+
includeRoutines: z.boolean().optional().default(false),
|
|
26
|
+
maxTables: z.number().min(1).optional(),
|
|
27
|
+
excludeSchemas: z.array(z.string()).optional(),
|
|
28
|
+
includeSchemas: z.array(z.string()).optional()
|
|
29
|
+
});
|
|
30
|
+
var DatabaseConfigSchema = z.object({
|
|
31
|
+
id: z.string().min(1),
|
|
32
|
+
type: z.enum(["postgres", "mysql", "mssql", "sqlite", "oracle"]),
|
|
33
|
+
url: z.string().optional(),
|
|
34
|
+
path: z.string().optional(),
|
|
35
|
+
readOnly: z.boolean().optional().default(true),
|
|
36
|
+
pool: ConnectionPoolSchema.optional(),
|
|
37
|
+
introspection: IntrospectionOptionsSchema.optional(),
|
|
38
|
+
eagerConnect: z.boolean().optional().default(false)
|
|
39
|
+
});
|
|
40
|
+
var ServerConfigSchema = z.object({
|
|
41
|
+
databases: z.array(DatabaseConfigSchema).min(1),
|
|
42
|
+
cache: z.object({
|
|
43
|
+
directory: z.string().optional().default(".sql-mcp-cache"),
|
|
44
|
+
ttlMinutes: z.number().min(0).optional().default(10)
|
|
45
|
+
}).optional().default({ directory: ".sql-mcp-cache", ttlMinutes: 10 }),
|
|
46
|
+
security: z.object({
|
|
47
|
+
allowWrite: z.boolean().optional().default(false),
|
|
48
|
+
allowedWriteOperations: z.array(z.string()).optional(),
|
|
49
|
+
disableDangerousOperations: z.boolean().optional().default(true),
|
|
50
|
+
redactSecrets: z.boolean().optional().default(true)
|
|
51
|
+
}).optional().default({ allowWrite: false, disableDangerousOperations: true, redactSecrets: true }),
|
|
52
|
+
logging: z.object({
|
|
53
|
+
level: z.enum(["trace", "debug", "info", "warn", "error"]).optional().default("info"),
|
|
54
|
+
pretty: z.boolean().optional().default(false)
|
|
55
|
+
}).optional().default({ level: "info", pretty: false })
|
|
56
|
+
});
|
|
57
|
+
var DatabaseError = class extends Error {
|
|
58
|
+
constructor(message, _code, _dbId, _originalError) {
|
|
59
|
+
super(message);
|
|
60
|
+
this._code = _code;
|
|
61
|
+
this._dbId = _dbId;
|
|
62
|
+
this._originalError = _originalError;
|
|
63
|
+
this.name = "DatabaseError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var ConfigError = class extends Error {
|
|
67
|
+
constructor(message, _details) {
|
|
68
|
+
super(message);
|
|
69
|
+
this._details = _details;
|
|
70
|
+
this.name = "ConfigError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var CacheError = class extends Error {
|
|
74
|
+
constructor(message, _originalError) {
|
|
75
|
+
super(message);
|
|
76
|
+
this._originalError = _originalError;
|
|
77
|
+
this.name = "CacheError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/utils.ts
|
|
82
|
+
import crypto from "crypto";
|
|
83
|
+
import { URL } from "url";
|
|
84
|
+
function redactUrl(url) {
|
|
85
|
+
try {
|
|
86
|
+
if (url.includes("://")) {
|
|
87
|
+
const urlObj = new URL(url);
|
|
88
|
+
if (urlObj.password) {
|
|
89
|
+
urlObj.password = "***";
|
|
90
|
+
}
|
|
91
|
+
if (urlObj.username && urlObj.password) {
|
|
92
|
+
return urlObj.toString();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (url.includes("Password=")) {
|
|
96
|
+
return url.replace(/(Password=)[^;]+/gi, "$1***");
|
|
97
|
+
}
|
|
98
|
+
if (url.includes("/") && url.includes("@")) {
|
|
99
|
+
return url.replace(/\/[^@]+@/, "/***@");
|
|
100
|
+
}
|
|
101
|
+
return url;
|
|
102
|
+
} catch {
|
|
103
|
+
return url.replace(/:\/\/[^@]*@/, "://***@");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function interpolateEnv(value) {
|
|
107
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
|
|
108
|
+
return process.env[varName] || "";
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function generateSchemaVersion(schema) {
|
|
112
|
+
const hash = crypto.createHash("sha256");
|
|
113
|
+
const schemaData = {
|
|
114
|
+
dbType: schema.dbType,
|
|
115
|
+
schemas: schema.schemas.map((s) => ({
|
|
116
|
+
name: s.name,
|
|
117
|
+
tables: s.tables.sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({
|
|
118
|
+
name: t.name,
|
|
119
|
+
type: t.type,
|
|
120
|
+
columns: t.columns.sort((a, b) => a.name.localeCompare(b.name)).map((c) => ({
|
|
121
|
+
name: c.name,
|
|
122
|
+
dataType: c.dataType,
|
|
123
|
+
nullable: c.nullable
|
|
124
|
+
})),
|
|
125
|
+
foreignKeys: t.foreignKeys.sort((a, b) => a.name.localeCompare(b.name))
|
|
126
|
+
}))
|
|
127
|
+
}))
|
|
128
|
+
};
|
|
129
|
+
hash.update(JSON.stringify(schemaData));
|
|
130
|
+
return hash.digest("hex").substring(0, 16);
|
|
131
|
+
}
|
|
132
|
+
function inferRelationships(schema) {
|
|
133
|
+
const relationships = [];
|
|
134
|
+
const tableLookup = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const schemaObj of schema.schemas) {
|
|
136
|
+
for (const table of schemaObj.tables) {
|
|
137
|
+
const fullName = `${schemaObj.name}.${table.name}`;
|
|
138
|
+
const pk = table.primaryKey?.columns || [];
|
|
139
|
+
tableLookup.set(table.name.toLowerCase(), { schema: schemaObj.name, pk });
|
|
140
|
+
tableLookup.set(fullName.toLowerCase(), { schema: schemaObj.name, pk });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const schemaObj of schema.schemas) {
|
|
144
|
+
for (const table of schemaObj.tables) {
|
|
145
|
+
for (const column of table.columns) {
|
|
146
|
+
const columnName = column.name.toLowerCase();
|
|
147
|
+
const patterns = [
|
|
148
|
+
/^(.+?)_id$/,
|
|
149
|
+
/^(.+?)id$/i
|
|
150
|
+
];
|
|
151
|
+
for (const pattern of patterns) {
|
|
152
|
+
const match = columnName.match(pattern);
|
|
153
|
+
if (match) {
|
|
154
|
+
const referencedTableName = match[1].toLowerCase();
|
|
155
|
+
const referencedTable = tableLookup.get(referencedTableName);
|
|
156
|
+
if (referencedTable && referencedTable.pk.length > 0) {
|
|
157
|
+
relationships.push({
|
|
158
|
+
fromSchema: schemaObj.name,
|
|
159
|
+
fromTable: table.name,
|
|
160
|
+
fromColumns: [column.name],
|
|
161
|
+
toSchema: referencedTable.schema,
|
|
162
|
+
toTable: referencedTableName,
|
|
163
|
+
toColumns: referencedTable.pk,
|
|
164
|
+
type: "inferred",
|
|
165
|
+
confidence: 0.7
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return relationships;
|
|
174
|
+
}
|
|
175
|
+
function extractTableNames(sql) {
|
|
176
|
+
const tables = /* @__PURE__ */ new Set();
|
|
177
|
+
const patterns = [
|
|
178
|
+
/(?:FROM|JOIN|INTO|UPDATE)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi,
|
|
179
|
+
/DELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)?)/gi
|
|
180
|
+
];
|
|
181
|
+
for (const pattern of patterns) {
|
|
182
|
+
const matches = sql.matchAll(pattern);
|
|
183
|
+
for (const match of matches) {
|
|
184
|
+
if (match[1]) {
|
|
185
|
+
tables.add(match[1].toLowerCase());
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return Array.from(tables);
|
|
190
|
+
}
|
|
191
|
+
function isWriteOperation(sql) {
|
|
192
|
+
const upperSql = sql.trim().toUpperCase();
|
|
193
|
+
const writeKeywords = [
|
|
194
|
+
"INSERT",
|
|
195
|
+
"UPDATE",
|
|
196
|
+
"DELETE",
|
|
197
|
+
"CREATE",
|
|
198
|
+
"ALTER",
|
|
199
|
+
"DROP",
|
|
200
|
+
"TRUNCATE",
|
|
201
|
+
"REPLACE",
|
|
202
|
+
"MERGE"
|
|
203
|
+
];
|
|
204
|
+
for (const keyword of writeKeywords) {
|
|
205
|
+
if (upperSql.startsWith(keyword)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function findJoinPaths(tables, relationships, maxDepth = 3) {
|
|
212
|
+
if (tables.length < 2) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const paths = [];
|
|
216
|
+
const graph = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const rel of relationships) {
|
|
218
|
+
const fromKey = `${rel.fromSchema}.${rel.fromTable}`.toLowerCase();
|
|
219
|
+
const toKey = `${rel.toSchema}.${rel.toTable}`.toLowerCase();
|
|
220
|
+
if (!graph.has(fromKey)) {
|
|
221
|
+
graph.set(fromKey, []);
|
|
222
|
+
}
|
|
223
|
+
graph.get(fromKey).push(rel);
|
|
224
|
+
const reverseRel = {
|
|
225
|
+
...rel,
|
|
226
|
+
fromSchema: rel.toSchema,
|
|
227
|
+
fromTable: rel.toTable,
|
|
228
|
+
fromColumns: rel.toColumns,
|
|
229
|
+
toSchema: rel.fromSchema,
|
|
230
|
+
toTable: rel.fromTable,
|
|
231
|
+
toColumns: rel.fromColumns
|
|
232
|
+
};
|
|
233
|
+
if (!graph.has(toKey)) {
|
|
234
|
+
graph.set(toKey, []);
|
|
235
|
+
}
|
|
236
|
+
graph.get(toKey).push(reverseRel);
|
|
237
|
+
}
|
|
238
|
+
const start = tables[0].toLowerCase();
|
|
239
|
+
const end = tables[1].toLowerCase();
|
|
240
|
+
const queue = [{ current: start, path: [] }];
|
|
241
|
+
const visited = /* @__PURE__ */ new Set([start]);
|
|
242
|
+
while (queue.length > 0) {
|
|
243
|
+
const { current, path: path2 } = queue.shift();
|
|
244
|
+
if (path2.length >= maxDepth) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const neighbors = graph.get(current) || [];
|
|
248
|
+
for (const rel of neighbors) {
|
|
249
|
+
const next = `${rel.toSchema}.${rel.toTable}`.toLowerCase();
|
|
250
|
+
if (next === end) {
|
|
251
|
+
paths.push({
|
|
252
|
+
tables: [start, ...path2.map((r) => `${r.toSchema}.${r.toTable}`), end],
|
|
253
|
+
joins: [...path2, rel]
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!visited.has(next)) {
|
|
258
|
+
visited.add(next);
|
|
259
|
+
queue.push({ current: next, path: [...path2, rel] });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return paths;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/config.ts
|
|
267
|
+
function findConfigFile(fileName, startDir = process.cwd()) {
|
|
268
|
+
let currentDir = resolve(startDir);
|
|
269
|
+
while (true) {
|
|
270
|
+
const configPath = join(currentDir, fileName);
|
|
271
|
+
if (existsSync(configPath)) {
|
|
272
|
+
return configPath;
|
|
273
|
+
}
|
|
274
|
+
const parentDir = dirname(currentDir);
|
|
275
|
+
if (parentDir === currentDir) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
currentDir = parentDir;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
async function loadConfig(configPath) {
|
|
283
|
+
try {
|
|
284
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
285
|
+
const rawConfig = JSON.parse(content);
|
|
286
|
+
const interpolatedConfig = interpolateConfigValues(rawConfig);
|
|
287
|
+
const config = ServerConfigSchema.parse(interpolatedConfig);
|
|
288
|
+
validateDatabaseConfigs(config);
|
|
289
|
+
return config;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error.name === "ZodError") {
|
|
292
|
+
throw new ConfigError("Configuration validation failed", error.errors);
|
|
293
|
+
}
|
|
294
|
+
throw new ConfigError(`Failed to load config from ${configPath}: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function interpolateConfigValues(obj) {
|
|
298
|
+
if (typeof obj === "string") {
|
|
299
|
+
return interpolateEnv(obj);
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(obj)) {
|
|
302
|
+
return obj.map(interpolateConfigValues);
|
|
303
|
+
}
|
|
304
|
+
if (obj && typeof obj === "object") {
|
|
305
|
+
const result = {};
|
|
306
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
307
|
+
result[key] = interpolateConfigValues(value);
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
return obj;
|
|
312
|
+
}
|
|
313
|
+
function validateDatabaseConfigs(config) {
|
|
314
|
+
const ids = /* @__PURE__ */ new Set();
|
|
315
|
+
for (const db of config.databases) {
|
|
316
|
+
if (ids.has(db.id)) {
|
|
317
|
+
throw new ConfigError(`Duplicate database ID: ${db.id}`);
|
|
318
|
+
}
|
|
319
|
+
ids.add(db.id);
|
|
320
|
+
if (db.type === "sqlite") {
|
|
321
|
+
if (!db.path && !db.url) {
|
|
322
|
+
throw new ConfigError(`SQLite database ${db.id} requires 'path' or 'url'`);
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
if (!db.url) {
|
|
326
|
+
throw new ConfigError(`Database ${db.id} requires 'url'`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/adapters/postgres.ts
|
|
333
|
+
import pg from "pg";
|
|
334
|
+
|
|
335
|
+
// src/logger.ts
|
|
336
|
+
import pino from "pino";
|
|
337
|
+
var logger;
|
|
338
|
+
function initLogger(level = "info", pretty = false) {
|
|
339
|
+
logger = pino({
|
|
340
|
+
level,
|
|
341
|
+
transport: pretty ? {
|
|
342
|
+
target: "pino-pretty",
|
|
343
|
+
options: {
|
|
344
|
+
colorize: true,
|
|
345
|
+
translateTime: "SYS:standard",
|
|
346
|
+
ignore: "pid,hostname"
|
|
347
|
+
}
|
|
348
|
+
} : void 0
|
|
349
|
+
});
|
|
350
|
+
return logger;
|
|
351
|
+
}
|
|
352
|
+
function getLogger() {
|
|
353
|
+
if (!logger) {
|
|
354
|
+
logger = initLogger();
|
|
355
|
+
}
|
|
356
|
+
return logger;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/adapters/base.ts
|
|
360
|
+
var BaseAdapter = class {
|
|
361
|
+
constructor(_config) {
|
|
362
|
+
this._config = _config;
|
|
363
|
+
}
|
|
364
|
+
logger = getLogger();
|
|
365
|
+
connected = false;
|
|
366
|
+
ensureConnected() {
|
|
367
|
+
if (!this.connected) {
|
|
368
|
+
throw new DatabaseError(
|
|
369
|
+
"Database not connected",
|
|
370
|
+
"NOT_CONNECTED",
|
|
371
|
+
this._config.id
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
handleError(error, operation) {
|
|
376
|
+
this.logger.error({ error, dbId: this._config.id, operation }, "Database operation failed");
|
|
377
|
+
throw new DatabaseError(
|
|
378
|
+
`${operation} failed: ${error.message}`,
|
|
379
|
+
error.code || "UNKNOWN_ERROR",
|
|
380
|
+
this._config.id,
|
|
381
|
+
error
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/adapters/postgres.ts
|
|
387
|
+
var { Pool } = pg;
|
|
388
|
+
var PostgresAdapter = class extends BaseAdapter {
|
|
389
|
+
pool;
|
|
390
|
+
async connect() {
|
|
391
|
+
try {
|
|
392
|
+
this.pool = new Pool({
|
|
393
|
+
connectionString: this._config.url,
|
|
394
|
+
min: this._config.pool?.min || 2,
|
|
395
|
+
max: this._config.pool?.max || 10,
|
|
396
|
+
idleTimeoutMillis: this._config.pool?.idleTimeoutMillis || 3e4,
|
|
397
|
+
connectionTimeoutMillis: this._config.pool?.connectionTimeoutMillis || 1e4
|
|
398
|
+
});
|
|
399
|
+
const client = await this.pool.connect();
|
|
400
|
+
client.release();
|
|
401
|
+
this.connected = true;
|
|
402
|
+
this.logger.info({ dbId: this._config.id }, "PostgreSQL connected");
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.handleError(error, "connect");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async disconnect() {
|
|
408
|
+
if (this.pool) {
|
|
409
|
+
await this.pool.end();
|
|
410
|
+
this.pool = void 0;
|
|
411
|
+
this.connected = false;
|
|
412
|
+
this.logger.info({ dbId: this._config.id }, "PostgreSQL disconnected");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async introspect(options) {
|
|
416
|
+
this.ensureConnected();
|
|
417
|
+
try {
|
|
418
|
+
const schemas = await this.getSchemas(options);
|
|
419
|
+
const dbSchema = {
|
|
420
|
+
dbId: this._config.id,
|
|
421
|
+
dbType: "postgres",
|
|
422
|
+
schemas,
|
|
423
|
+
introspectedAt: /* @__PURE__ */ new Date(),
|
|
424
|
+
version: ""
|
|
425
|
+
};
|
|
426
|
+
dbSchema.version = generateSchemaVersion(dbSchema);
|
|
427
|
+
return dbSchema;
|
|
428
|
+
} catch (error) {
|
|
429
|
+
this.handleError(error, "introspect");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async getSchemas(options) {
|
|
433
|
+
const result = [];
|
|
434
|
+
const schemasQuery = `
|
|
435
|
+
SELECT schema_name
|
|
436
|
+
FROM information_schema.schemata
|
|
437
|
+
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
438
|
+
ORDER BY schema_name
|
|
439
|
+
`;
|
|
440
|
+
const schemasResult = await this.pool.query(schemasQuery);
|
|
441
|
+
let schemaNames = schemasResult.rows.map((r) => r.schema_name);
|
|
442
|
+
if (options?.includeSchemas && options.includeSchemas.length > 0) {
|
|
443
|
+
schemaNames = schemaNames.filter((s) => options.includeSchemas.includes(s));
|
|
444
|
+
}
|
|
445
|
+
if (options?.excludeSchemas && options.excludeSchemas.length > 0) {
|
|
446
|
+
schemaNames = schemaNames.filter((s) => !options.excludeSchemas.includes(s));
|
|
447
|
+
}
|
|
448
|
+
for (const schemaName of schemaNames) {
|
|
449
|
+
const tables = await this.getTables(schemaName, options);
|
|
450
|
+
result.push({
|
|
451
|
+
name: schemaName,
|
|
452
|
+
tables
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
457
|
+
async getTables(schemaName, options) {
|
|
458
|
+
const result = [];
|
|
459
|
+
let tableTypes = "'BASE TABLE'";
|
|
460
|
+
if (options?.includeViews) {
|
|
461
|
+
tableTypes += ",'VIEW'";
|
|
462
|
+
}
|
|
463
|
+
const tablesQuery = `
|
|
464
|
+
SELECT table_name, table_type
|
|
465
|
+
FROM information_schema.tables
|
|
466
|
+
WHERE table_schema = $1 AND table_type IN (${tableTypes})
|
|
467
|
+
ORDER BY table_name
|
|
468
|
+
${options?.maxTables ? `LIMIT ${options.maxTables}` : ""}
|
|
469
|
+
`;
|
|
470
|
+
const tablesResult = await this.pool.query(tablesQuery, [schemaName]);
|
|
471
|
+
for (const row of tablesResult.rows) {
|
|
472
|
+
const columns = await this.getColumns(schemaName, row.table_name);
|
|
473
|
+
const indexes = await this.getIndexes(schemaName, row.table_name);
|
|
474
|
+
const foreignKeys = await this.getForeignKeys(schemaName, row.table_name);
|
|
475
|
+
const primaryKey = indexes.find((idx) => idx.isPrimary);
|
|
476
|
+
result.push({
|
|
477
|
+
schema: schemaName,
|
|
478
|
+
name: row.table_name,
|
|
479
|
+
type: row.table_type === "VIEW" ? "view" : "table",
|
|
480
|
+
columns,
|
|
481
|
+
primaryKey,
|
|
482
|
+
indexes: indexes.filter((idx) => !idx.isPrimary),
|
|
483
|
+
foreignKeys
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
async getColumns(schemaName, tableName) {
|
|
489
|
+
const query = `
|
|
490
|
+
SELECT
|
|
491
|
+
column_name,
|
|
492
|
+
data_type,
|
|
493
|
+
is_nullable,
|
|
494
|
+
column_default,
|
|
495
|
+
character_maximum_length,
|
|
496
|
+
numeric_precision,
|
|
497
|
+
numeric_scale,
|
|
498
|
+
is_identity
|
|
499
|
+
FROM information_schema.columns
|
|
500
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
501
|
+
ORDER BY ordinal_position
|
|
502
|
+
`;
|
|
503
|
+
const result = await this.pool.query(query, [schemaName, tableName]);
|
|
504
|
+
return result.rows.map((row) => ({
|
|
505
|
+
name: row.column_name,
|
|
506
|
+
dataType: row.data_type,
|
|
507
|
+
nullable: row.is_nullable === "YES",
|
|
508
|
+
defaultValue: row.column_default,
|
|
509
|
+
maxLength: row.character_maximum_length,
|
|
510
|
+
precision: row.numeric_precision,
|
|
511
|
+
scale: row.numeric_scale,
|
|
512
|
+
isAutoIncrement: row.is_identity === "YES"
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
async getIndexes(schemaName, tableName) {
|
|
516
|
+
const query = `
|
|
517
|
+
SELECT
|
|
518
|
+
i.relname AS index_name,
|
|
519
|
+
ix.indisunique AS is_unique,
|
|
520
|
+
ix.indisprimary AS is_primary,
|
|
521
|
+
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) AS column_names
|
|
522
|
+
FROM pg_class t
|
|
523
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
524
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
525
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
526
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
527
|
+
WHERE n.nspname = $1 AND t.relname = $2
|
|
528
|
+
GROUP BY i.relname, ix.indisunique, ix.indisprimary
|
|
529
|
+
`;
|
|
530
|
+
const result = await this.pool.query(query, [schemaName, tableName]);
|
|
531
|
+
return result.rows.map((row) => ({
|
|
532
|
+
name: row.index_name,
|
|
533
|
+
columns: row.column_names,
|
|
534
|
+
isUnique: row.is_unique,
|
|
535
|
+
isPrimary: row.is_primary
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
async getForeignKeys(schemaName, tableName) {
|
|
539
|
+
const query = `
|
|
540
|
+
SELECT
|
|
541
|
+
tc.constraint_name,
|
|
542
|
+
array_agg(kcu.column_name ORDER BY kcu.ordinal_position) AS column_names,
|
|
543
|
+
ccu.table_schema AS referenced_schema,
|
|
544
|
+
ccu.table_name AS referenced_table,
|
|
545
|
+
array_agg(ccu.column_name ORDER BY kcu.ordinal_position) AS referenced_columns,
|
|
546
|
+
rc.update_rule,
|
|
547
|
+
rc.delete_rule
|
|
548
|
+
FROM information_schema.table_constraints AS tc
|
|
549
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
550
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
551
|
+
AND tc.table_schema = kcu.table_schema
|
|
552
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
553
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
554
|
+
AND ccu.table_schema = tc.table_schema
|
|
555
|
+
JOIN information_schema.referential_constraints AS rc
|
|
556
|
+
ON rc.constraint_name = tc.constraint_name
|
|
557
|
+
AND rc.constraint_schema = tc.table_schema
|
|
558
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
559
|
+
AND tc.table_schema = $1
|
|
560
|
+
AND tc.table_name = $2
|
|
561
|
+
GROUP BY tc.constraint_name, ccu.table_schema, ccu.table_name, rc.update_rule, rc.delete_rule
|
|
562
|
+
`;
|
|
563
|
+
const result = await this.pool.query(query, [schemaName, tableName]);
|
|
564
|
+
return result.rows.map((row) => ({
|
|
565
|
+
name: row.constraint_name,
|
|
566
|
+
columns: row.column_names,
|
|
567
|
+
referencedSchema: row.referenced_schema,
|
|
568
|
+
referencedTable: row.referenced_table,
|
|
569
|
+
referencedColumns: row.referenced_columns,
|
|
570
|
+
onUpdate: row.update_rule,
|
|
571
|
+
onDelete: row.delete_rule
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
async query(sql, params = [], timeoutMs) {
|
|
575
|
+
this.ensureConnected();
|
|
576
|
+
const startTime = Date.now();
|
|
577
|
+
try {
|
|
578
|
+
const queryConfig = {
|
|
579
|
+
text: sql,
|
|
580
|
+
values: params
|
|
581
|
+
};
|
|
582
|
+
if (timeoutMs) {
|
|
583
|
+
queryConfig.statement_timeout = timeoutMs;
|
|
584
|
+
}
|
|
585
|
+
const result = await this.pool.query(queryConfig);
|
|
586
|
+
const executionTimeMs = Date.now() - startTime;
|
|
587
|
+
return {
|
|
588
|
+
rows: result.rows,
|
|
589
|
+
columns: result.fields.map((f) => f.name),
|
|
590
|
+
rowCount: result.rowCount || 0,
|
|
591
|
+
executionTimeMs,
|
|
592
|
+
affectedRows: result.rowCount || 0
|
|
593
|
+
};
|
|
594
|
+
} catch (error) {
|
|
595
|
+
this.handleError(error, "query");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async explain(sql, params = []) {
|
|
599
|
+
this.ensureConnected();
|
|
600
|
+
try {
|
|
601
|
+
const explainSql = `EXPLAIN (FORMAT JSON, ANALYZE, BUFFERS) ${sql}`;
|
|
602
|
+
const result = await this.pool.query(explainSql, params);
|
|
603
|
+
return {
|
|
604
|
+
plan: result.rows[0]["QUERY PLAN"],
|
|
605
|
+
formattedPlan: JSON.stringify(result.rows[0]["QUERY PLAN"], null, 2)
|
|
606
|
+
};
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this.handleError(error, "explain");
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async testConnection() {
|
|
612
|
+
try {
|
|
613
|
+
if (!this.pool) return false;
|
|
614
|
+
const client = await this.pool.connect();
|
|
615
|
+
client.release();
|
|
616
|
+
return true;
|
|
617
|
+
} catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async getVersion() {
|
|
622
|
+
this.ensureConnected();
|
|
623
|
+
try {
|
|
624
|
+
const result = await this.pool.query("SELECT version()");
|
|
625
|
+
return result.rows[0].version;
|
|
626
|
+
} catch (error) {
|
|
627
|
+
this.handleError(error, "getVersion");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/adapters/mysql.ts
|
|
633
|
+
import mysql from "mysql2/promise";
|
|
634
|
+
var MySQLAdapter = class extends BaseAdapter {
|
|
635
|
+
pool;
|
|
636
|
+
database;
|
|
637
|
+
async connect() {
|
|
638
|
+
try {
|
|
639
|
+
this.pool = mysql.createPool({
|
|
640
|
+
uri: this._config.url,
|
|
641
|
+
waitForConnections: true,
|
|
642
|
+
connectionLimit: this._config.pool?.max || 10,
|
|
643
|
+
queueLimit: 0,
|
|
644
|
+
connectTimeout: this._config.pool?.connectionTimeoutMillis || 1e4
|
|
645
|
+
});
|
|
646
|
+
const connection = await this.pool.getConnection();
|
|
647
|
+
const [rows] = await connection.query("SELECT DATABASE() as db");
|
|
648
|
+
this.database = rows[0].db;
|
|
649
|
+
connection.release();
|
|
650
|
+
this.connected = true;
|
|
651
|
+
this.logger.info({ dbId: this._config.id }, "MySQL connected");
|
|
652
|
+
} catch (error) {
|
|
653
|
+
this.handleError(error, "connect");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async disconnect() {
|
|
657
|
+
if (this.pool) {
|
|
658
|
+
await this.pool.end();
|
|
659
|
+
this.pool = void 0;
|
|
660
|
+
this.connected = false;
|
|
661
|
+
this.logger.info({ dbId: this._config.id }, "MySQL disconnected");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async introspect(options) {
|
|
665
|
+
this.ensureConnected();
|
|
666
|
+
try {
|
|
667
|
+
const schemas = await this.getSchemas(options);
|
|
668
|
+
const dbSchema = {
|
|
669
|
+
dbId: this._config.id,
|
|
670
|
+
dbType: "mysql",
|
|
671
|
+
schemas,
|
|
672
|
+
introspectedAt: /* @__PURE__ */ new Date(),
|
|
673
|
+
version: ""
|
|
674
|
+
};
|
|
675
|
+
dbSchema.version = generateSchemaVersion(dbSchema);
|
|
676
|
+
return dbSchema;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.handleError(error, "introspect");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async getSchemas(options) {
|
|
682
|
+
const tables = await this.getTables(this.database, options);
|
|
683
|
+
return [
|
|
684
|
+
{
|
|
685
|
+
name: this.database,
|
|
686
|
+
tables
|
|
687
|
+
}
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
async getTables(schemaName, options) {
|
|
691
|
+
const result = [];
|
|
692
|
+
let tableTypes = "'BASE TABLE'";
|
|
693
|
+
if (options?.includeViews) {
|
|
694
|
+
tableTypes += ",'VIEW'";
|
|
695
|
+
}
|
|
696
|
+
const tablesQuery = `
|
|
697
|
+
SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT
|
|
698
|
+
FROM information_schema.TABLES
|
|
699
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_TYPE IN (${tableTypes})
|
|
700
|
+
ORDER BY TABLE_NAME
|
|
701
|
+
${options?.maxTables ? `LIMIT ${options.maxTables}` : ""}
|
|
702
|
+
`;
|
|
703
|
+
const [rows] = await this.pool.query(tablesQuery, [schemaName]);
|
|
704
|
+
for (const row of rows) {
|
|
705
|
+
const columns = await this.getColumns(schemaName, row.TABLE_NAME);
|
|
706
|
+
const indexes = await this.getIndexes(schemaName, row.TABLE_NAME);
|
|
707
|
+
const foreignKeys = await this.getForeignKeys(schemaName, row.TABLE_NAME);
|
|
708
|
+
const primaryKey = indexes.find((idx) => idx.isPrimary);
|
|
709
|
+
result.push({
|
|
710
|
+
schema: schemaName,
|
|
711
|
+
name: row.TABLE_NAME,
|
|
712
|
+
type: row.TABLE_TYPE === "VIEW" ? "view" : "table",
|
|
713
|
+
columns,
|
|
714
|
+
primaryKey,
|
|
715
|
+
indexes: indexes.filter((idx) => !idx.isPrimary),
|
|
716
|
+
foreignKeys,
|
|
717
|
+
comment: row.TABLE_COMMENT
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
async getColumns(schemaName, tableName) {
|
|
723
|
+
const query = `
|
|
724
|
+
SELECT
|
|
725
|
+
COLUMN_NAME,
|
|
726
|
+
DATA_TYPE,
|
|
727
|
+
IS_NULLABLE,
|
|
728
|
+
COLUMN_DEFAULT,
|
|
729
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
730
|
+
NUMERIC_PRECISION,
|
|
731
|
+
NUMERIC_SCALE,
|
|
732
|
+
EXTRA,
|
|
733
|
+
COLUMN_COMMENT
|
|
734
|
+
FROM information_schema.COLUMNS
|
|
735
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
736
|
+
ORDER BY ORDINAL_POSITION
|
|
737
|
+
`;
|
|
738
|
+
const [rows] = await this.pool.query(query, [schemaName, tableName]);
|
|
739
|
+
return rows.map((row) => ({
|
|
740
|
+
name: row.COLUMN_NAME,
|
|
741
|
+
dataType: row.DATA_TYPE,
|
|
742
|
+
nullable: row.IS_NULLABLE === "YES",
|
|
743
|
+
defaultValue: row.COLUMN_DEFAULT,
|
|
744
|
+
maxLength: row.CHARACTER_MAXIMUM_LENGTH,
|
|
745
|
+
precision: row.NUMERIC_PRECISION,
|
|
746
|
+
scale: row.NUMERIC_SCALE,
|
|
747
|
+
isAutoIncrement: row.EXTRA.includes("auto_increment"),
|
|
748
|
+
comment: row.COLUMN_COMMENT
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
async getIndexes(schemaName, tableName) {
|
|
752
|
+
const query = `
|
|
753
|
+
SELECT
|
|
754
|
+
INDEX_NAME,
|
|
755
|
+
NON_UNIQUE,
|
|
756
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS column_names
|
|
757
|
+
FROM information_schema.STATISTICS
|
|
758
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
|
759
|
+
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
760
|
+
`;
|
|
761
|
+
const [rows] = await this.pool.query(query, [schemaName, tableName]);
|
|
762
|
+
return rows.map((row) => ({
|
|
763
|
+
name: row.INDEX_NAME,
|
|
764
|
+
columns: row.column_names.split(","),
|
|
765
|
+
isUnique: row.NON_UNIQUE === 0,
|
|
766
|
+
isPrimary: row.INDEX_NAME === "PRIMARY"
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
async getForeignKeys(schemaName, tableName) {
|
|
770
|
+
const query = `
|
|
771
|
+
SELECT
|
|
772
|
+
kcu.CONSTRAINT_NAME,
|
|
773
|
+
GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) AS column_names,
|
|
774
|
+
kcu.REFERENCED_TABLE_SCHEMA,
|
|
775
|
+
kcu.REFERENCED_TABLE_NAME,
|
|
776
|
+
GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) AS referenced_columns,
|
|
777
|
+
rc.UPDATE_RULE,
|
|
778
|
+
rc.DELETE_RULE
|
|
779
|
+
FROM information_schema.KEY_COLUMN_USAGE AS kcu
|
|
780
|
+
JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
|
|
781
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
782
|
+
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
783
|
+
WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ?
|
|
784
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
785
|
+
GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_SCHEMA,
|
|
786
|
+
kcu.REFERENCED_TABLE_NAME, rc.UPDATE_RULE, rc.DELETE_RULE
|
|
787
|
+
`;
|
|
788
|
+
const [rows] = await this.pool.query(query, [schemaName, tableName]);
|
|
789
|
+
return rows.map((row) => ({
|
|
790
|
+
name: row.CONSTRAINT_NAME,
|
|
791
|
+
columns: row.column_names.split(","),
|
|
792
|
+
referencedSchema: row.REFERENCED_TABLE_SCHEMA,
|
|
793
|
+
referencedTable: row.REFERENCED_TABLE_NAME,
|
|
794
|
+
referencedColumns: row.referenced_columns.split(","),
|
|
795
|
+
onUpdate: row.UPDATE_RULE,
|
|
796
|
+
onDelete: row.DELETE_RULE
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
async query(sql, params = [], timeoutMs) {
|
|
800
|
+
this.ensureConnected();
|
|
801
|
+
const startTime = Date.now();
|
|
802
|
+
try {
|
|
803
|
+
const connection = await this.pool.getConnection();
|
|
804
|
+
if (timeoutMs) {
|
|
805
|
+
await connection.query(`SET SESSION max_execution_time=${timeoutMs}`);
|
|
806
|
+
}
|
|
807
|
+
const [rows, fields] = await connection.query(sql, params);
|
|
808
|
+
connection.release();
|
|
809
|
+
const executionTimeMs = Date.now() - startTime;
|
|
810
|
+
return {
|
|
811
|
+
rows: Array.isArray(rows) ? rows : [],
|
|
812
|
+
columns: Array.isArray(fields) ? fields.map((f) => f.name) : [],
|
|
813
|
+
rowCount: Array.isArray(rows) ? rows.length : 0,
|
|
814
|
+
executionTimeMs,
|
|
815
|
+
affectedRows: rows.affectedRows
|
|
816
|
+
};
|
|
817
|
+
} catch (error) {
|
|
818
|
+
this.handleError(error, "query");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async explain(sql, params = []) {
|
|
822
|
+
this.ensureConnected();
|
|
823
|
+
try {
|
|
824
|
+
const explainSql = `EXPLAIN FORMAT=JSON ${sql}`;
|
|
825
|
+
const [rows] = await this.pool.query(explainSql, params);
|
|
826
|
+
return {
|
|
827
|
+
plan: rows[0].EXPLAIN,
|
|
828
|
+
formattedPlan: JSON.stringify(rows[0].EXPLAIN, null, 2)
|
|
829
|
+
};
|
|
830
|
+
} catch (error) {
|
|
831
|
+
this.handleError(error, "explain");
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async testConnection() {
|
|
835
|
+
try {
|
|
836
|
+
if (!this.pool) return false;
|
|
837
|
+
const connection = await this.pool.getConnection();
|
|
838
|
+
connection.release();
|
|
839
|
+
return true;
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async getVersion() {
|
|
845
|
+
this.ensureConnected();
|
|
846
|
+
try {
|
|
847
|
+
const [rows] = await this.pool.query("SELECT VERSION() as version");
|
|
848
|
+
return rows[0].version;
|
|
849
|
+
} catch (error) {
|
|
850
|
+
this.handleError(error, "getVersion");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// src/adapters/sqlite.ts
|
|
856
|
+
import Database from "better-sqlite3";
|
|
857
|
+
var SQLiteAdapter = class extends BaseAdapter {
|
|
858
|
+
db;
|
|
859
|
+
async connect() {
|
|
860
|
+
try {
|
|
861
|
+
const dbPath = this._config.path || this._config.url;
|
|
862
|
+
if (!dbPath) {
|
|
863
|
+
throw new Error("SQLite requires path or url configuration");
|
|
864
|
+
}
|
|
865
|
+
this.db = new Database(dbPath, {
|
|
866
|
+
readonly: this._config.readOnly,
|
|
867
|
+
fileMustExist: false
|
|
868
|
+
});
|
|
869
|
+
this.db.pragma("foreign_keys = ON");
|
|
870
|
+
this.connected = true;
|
|
871
|
+
this.logger.info({ dbId: this._config.id, path: dbPath }, "SQLite connected");
|
|
872
|
+
} catch (error) {
|
|
873
|
+
this.handleError(error, "connect");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async disconnect() {
|
|
877
|
+
if (this.db) {
|
|
878
|
+
this.db.close();
|
|
879
|
+
this.db = void 0;
|
|
880
|
+
this.connected = false;
|
|
881
|
+
this.logger.info({ dbId: this._config.id }, "SQLite disconnected");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async introspect(options) {
|
|
885
|
+
this.ensureConnected();
|
|
886
|
+
try {
|
|
887
|
+
const schemas = await this.getSchemas(options);
|
|
888
|
+
const dbSchema = {
|
|
889
|
+
dbId: this._config.id,
|
|
890
|
+
dbType: "sqlite",
|
|
891
|
+
schemas,
|
|
892
|
+
introspectedAt: /* @__PURE__ */ new Date(),
|
|
893
|
+
version: ""
|
|
894
|
+
};
|
|
895
|
+
dbSchema.version = generateSchemaVersion(dbSchema);
|
|
896
|
+
return dbSchema;
|
|
897
|
+
} catch (error) {
|
|
898
|
+
this.handleError(error, "introspect");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async getSchemas(options) {
|
|
902
|
+
const tables = await this.getTables("main", options);
|
|
903
|
+
return [
|
|
904
|
+
{
|
|
905
|
+
name: "main",
|
|
906
|
+
tables
|
|
907
|
+
}
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
async getTables(schemaName, options) {
|
|
911
|
+
const result = [];
|
|
912
|
+
let query = `
|
|
913
|
+
SELECT name, type
|
|
914
|
+
FROM sqlite_master
|
|
915
|
+
WHERE type IN ('table', 'view')
|
|
916
|
+
AND name NOT LIKE 'sqlite_%'
|
|
917
|
+
ORDER BY name
|
|
918
|
+
`;
|
|
919
|
+
if (options?.maxTables) {
|
|
920
|
+
query += ` LIMIT ${options.maxTables}`;
|
|
921
|
+
}
|
|
922
|
+
const tables = this.db.prepare(query).all();
|
|
923
|
+
for (const table of tables) {
|
|
924
|
+
if (table.type === "view" && !options?.includeViews) {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const columns = await this.getColumns(table.name);
|
|
928
|
+
const indexes = await this.getIndexes(table.name);
|
|
929
|
+
const foreignKeys = await this.getForeignKeys(table.name);
|
|
930
|
+
const primaryKey = indexes.find((idx) => idx.isPrimary);
|
|
931
|
+
result.push({
|
|
932
|
+
schema: schemaName,
|
|
933
|
+
name: table.name,
|
|
934
|
+
type: table.type === "view" ? "view" : "table",
|
|
935
|
+
columns,
|
|
936
|
+
primaryKey,
|
|
937
|
+
indexes: indexes.filter((idx) => !idx.isPrimary),
|
|
938
|
+
foreignKeys
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
async getColumns(tableName) {
|
|
944
|
+
const pragma = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
945
|
+
return pragma.map((col) => ({
|
|
946
|
+
name: col.name,
|
|
947
|
+
dataType: col.type || "TEXT",
|
|
948
|
+
nullable: col.notnull === 0,
|
|
949
|
+
defaultValue: col.dflt_value || void 0,
|
|
950
|
+
isAutoIncrement: col.pk === 1 && col.type.toUpperCase() === "INTEGER"
|
|
951
|
+
}));
|
|
952
|
+
}
|
|
953
|
+
async getIndexes(tableName) {
|
|
954
|
+
const result = [];
|
|
955
|
+
const indexes = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
|
|
956
|
+
for (const index of indexes) {
|
|
957
|
+
const indexInfo = this.db.prepare(`PRAGMA index_info(${index.name})`).all();
|
|
958
|
+
result.push({
|
|
959
|
+
name: index.name,
|
|
960
|
+
columns: indexInfo.map((info) => info.name),
|
|
961
|
+
isUnique: index.unique === 1,
|
|
962
|
+
isPrimary: index.origin === "pk"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
966
|
+
}
|
|
967
|
+
async getForeignKeys(tableName) {
|
|
968
|
+
const foreignKeys = this.db.prepare(`PRAGMA foreign_key_list(${tableName})`).all();
|
|
969
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
970
|
+
for (const fk of foreignKeys) {
|
|
971
|
+
if (!grouped.has(fk.id)) {
|
|
972
|
+
grouped.set(fk.id, []);
|
|
973
|
+
}
|
|
974
|
+
grouped.get(fk.id).push(fk);
|
|
975
|
+
}
|
|
976
|
+
return Array.from(grouped.values()).map((fks) => ({
|
|
977
|
+
name: `fk_${tableName}_${fks[0].id}`,
|
|
978
|
+
columns: fks.map((fk) => fk.from),
|
|
979
|
+
referencedSchema: "main",
|
|
980
|
+
referencedTable: fks[0].table,
|
|
981
|
+
referencedColumns: fks.map((fk) => fk.to),
|
|
982
|
+
onUpdate: fks[0].on_update,
|
|
983
|
+
onDelete: fks[0].on_delete
|
|
984
|
+
}));
|
|
985
|
+
}
|
|
986
|
+
async query(sql, params = [], timeoutMs) {
|
|
987
|
+
this.ensureConnected();
|
|
988
|
+
const startTime = Date.now();
|
|
989
|
+
try {
|
|
990
|
+
if (timeoutMs) {
|
|
991
|
+
this.db.pragma(`busy_timeout = ${timeoutMs}`);
|
|
992
|
+
}
|
|
993
|
+
const stmt = this.db.prepare(sql);
|
|
994
|
+
const isSelect = sql.trim().toUpperCase().startsWith("SELECT");
|
|
995
|
+
let rows;
|
|
996
|
+
let affectedRows = 0;
|
|
997
|
+
if (isSelect) {
|
|
998
|
+
rows = stmt.all(...params);
|
|
999
|
+
} else {
|
|
1000
|
+
const result = stmt.run(...params);
|
|
1001
|
+
rows = [];
|
|
1002
|
+
affectedRows = result.changes;
|
|
1003
|
+
}
|
|
1004
|
+
const executionTimeMs = Date.now() - startTime;
|
|
1005
|
+
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
1006
|
+
return {
|
|
1007
|
+
rows,
|
|
1008
|
+
columns,
|
|
1009
|
+
rowCount: rows.length,
|
|
1010
|
+
executionTimeMs,
|
|
1011
|
+
affectedRows
|
|
1012
|
+
};
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
this.handleError(error, "query");
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
async explain(sql, params = []) {
|
|
1018
|
+
this.ensureConnected();
|
|
1019
|
+
try {
|
|
1020
|
+
const explainSql = `EXPLAIN QUERY PLAN ${sql}`;
|
|
1021
|
+
const stmt = this.db.prepare(explainSql);
|
|
1022
|
+
const plan = stmt.all(...params);
|
|
1023
|
+
return {
|
|
1024
|
+
plan,
|
|
1025
|
+
formattedPlan: JSON.stringify(plan, null, 2)
|
|
1026
|
+
};
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
this.handleError(error, "explain");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async testConnection() {
|
|
1032
|
+
try {
|
|
1033
|
+
if (!this.db) return false;
|
|
1034
|
+
this.db.prepare("SELECT 1").get();
|
|
1035
|
+
return true;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async getVersion() {
|
|
1041
|
+
this.ensureConnected();
|
|
1042
|
+
try {
|
|
1043
|
+
const result = this.db.prepare("SELECT sqlite_version() as version").get();
|
|
1044
|
+
return `SQLite ${result.version}`;
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
this.handleError(error, "getVersion");
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/adapters/mssql.ts
|
|
1052
|
+
import { Connection, Request } from "tedious";
|
|
1053
|
+
var MSSQLAdapter = class extends BaseAdapter {
|
|
1054
|
+
connection;
|
|
1055
|
+
async connect() {
|
|
1056
|
+
return new Promise((resolve2, reject) => {
|
|
1057
|
+
try {
|
|
1058
|
+
const config = this.parseConnectionString(this._config.url);
|
|
1059
|
+
this.connection = new Connection(config);
|
|
1060
|
+
this.connection.on("connect", (err) => {
|
|
1061
|
+
if (err) {
|
|
1062
|
+
reject(err);
|
|
1063
|
+
} else {
|
|
1064
|
+
this.connected = true;
|
|
1065
|
+
this.logger.info({ dbId: this._config.id }, "SQL Server connected");
|
|
1066
|
+
resolve2();
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
this.connection.connect();
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
this.handleError(error, "connect");
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
parseConnectionString(connStr) {
|
|
1076
|
+
const config = {
|
|
1077
|
+
options: {
|
|
1078
|
+
encrypt: true,
|
|
1079
|
+
trustServerCertificate: true,
|
|
1080
|
+
enableArithAbort: true
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
const parts = connStr.split(";").filter((p) => p.trim());
|
|
1084
|
+
for (const part of parts) {
|
|
1085
|
+
const [key, value] = part.split("=").map((s) => s.trim());
|
|
1086
|
+
const lowerKey = key.toLowerCase();
|
|
1087
|
+
if (lowerKey === "server") {
|
|
1088
|
+
const [host, port] = value.split(",");
|
|
1089
|
+
config.server = host;
|
|
1090
|
+
if (port) config.options.port = parseInt(port);
|
|
1091
|
+
} else if (lowerKey === "database") {
|
|
1092
|
+
config.options.database = value;
|
|
1093
|
+
} else if (lowerKey === "user id") {
|
|
1094
|
+
config.authentication = {
|
|
1095
|
+
type: "default",
|
|
1096
|
+
options: { userName: value, password: "" }
|
|
1097
|
+
};
|
|
1098
|
+
} else if (lowerKey === "password") {
|
|
1099
|
+
if (config.authentication) {
|
|
1100
|
+
config.authentication.options.password = value;
|
|
1101
|
+
}
|
|
1102
|
+
} else if (lowerKey === "encrypt") {
|
|
1103
|
+
config.options.encrypt = value.toLowerCase() === "true";
|
|
1104
|
+
} else if (lowerKey === "trustservercertificate") {
|
|
1105
|
+
config.options.trustServerCertificate = value.toLowerCase() === "true";
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return config;
|
|
1109
|
+
}
|
|
1110
|
+
async disconnect() {
|
|
1111
|
+
if (this.connection) {
|
|
1112
|
+
this.connection.close();
|
|
1113
|
+
this.connection = void 0;
|
|
1114
|
+
this.connected = false;
|
|
1115
|
+
this.logger.info({ dbId: this._config.id }, "SQL Server disconnected");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
executeQuery(sql, _params = []) {
|
|
1119
|
+
return new Promise((resolve2, reject) => {
|
|
1120
|
+
const rows = [];
|
|
1121
|
+
const request = new Request(sql, (err) => {
|
|
1122
|
+
if (err) {
|
|
1123
|
+
reject(err);
|
|
1124
|
+
} else {
|
|
1125
|
+
resolve2(rows);
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
request.on("row", (columns) => {
|
|
1129
|
+
const row = {};
|
|
1130
|
+
columns.forEach((col) => {
|
|
1131
|
+
row[col.metadata.colName] = col.value;
|
|
1132
|
+
});
|
|
1133
|
+
rows.push(row);
|
|
1134
|
+
});
|
|
1135
|
+
this.connection.execSql(request);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
async introspect(options) {
|
|
1139
|
+
this.ensureConnected();
|
|
1140
|
+
try {
|
|
1141
|
+
const schemas = await this.getSchemas(options);
|
|
1142
|
+
const dbSchema = {
|
|
1143
|
+
dbId: this._config.id,
|
|
1144
|
+
dbType: "mssql",
|
|
1145
|
+
schemas,
|
|
1146
|
+
introspectedAt: /* @__PURE__ */ new Date(),
|
|
1147
|
+
version: ""
|
|
1148
|
+
};
|
|
1149
|
+
dbSchema.version = generateSchemaVersion(dbSchema);
|
|
1150
|
+
return dbSchema;
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
this.handleError(error, "introspect");
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async getSchemas(options) {
|
|
1156
|
+
const result = [];
|
|
1157
|
+
const schemasQuery = `
|
|
1158
|
+
SELECT SCHEMA_NAME
|
|
1159
|
+
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
1160
|
+
WHERE SCHEMA_NAME NOT IN ('sys', 'INFORMATION_SCHEMA', 'guest')
|
|
1161
|
+
ORDER BY SCHEMA_NAME
|
|
1162
|
+
`;
|
|
1163
|
+
const schemasResult = await this.executeQuery(schemasQuery);
|
|
1164
|
+
let schemaNames = schemasResult.map((r) => r.SCHEMA_NAME);
|
|
1165
|
+
if (options?.includeSchemas && options.includeSchemas.length > 0) {
|
|
1166
|
+
schemaNames = schemaNames.filter((s) => options.includeSchemas.includes(s));
|
|
1167
|
+
}
|
|
1168
|
+
if (options?.excludeSchemas && options.excludeSchemas.length > 0) {
|
|
1169
|
+
schemaNames = schemaNames.filter((s) => !options.excludeSchemas.includes(s));
|
|
1170
|
+
}
|
|
1171
|
+
for (const schemaName of schemaNames) {
|
|
1172
|
+
const tables = await this.getTables(schemaName, options);
|
|
1173
|
+
result.push({
|
|
1174
|
+
name: schemaName,
|
|
1175
|
+
tables
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
return result;
|
|
1179
|
+
}
|
|
1180
|
+
async getTables(schemaName, options) {
|
|
1181
|
+
const result = [];
|
|
1182
|
+
let tableTypes = "'BASE TABLE'";
|
|
1183
|
+
if (options?.includeViews) {
|
|
1184
|
+
tableTypes += ",'VIEW'";
|
|
1185
|
+
}
|
|
1186
|
+
const tablesQuery = `
|
|
1187
|
+
SELECT TABLE_NAME, TABLE_TYPE
|
|
1188
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
1189
|
+
WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_TYPE IN (${tableTypes})
|
|
1190
|
+
ORDER BY TABLE_NAME
|
|
1191
|
+
${options?.maxTables ? `OFFSET 0 ROWS FETCH NEXT ${options.maxTables} ROWS ONLY` : ""}
|
|
1192
|
+
`;
|
|
1193
|
+
const tablesResult = await this.executeQuery(tablesQuery);
|
|
1194
|
+
for (const row of tablesResult) {
|
|
1195
|
+
const columns = await this.getColumns(schemaName, row.TABLE_NAME);
|
|
1196
|
+
const indexes = await this.getIndexes(schemaName, row.TABLE_NAME);
|
|
1197
|
+
const foreignKeys = await this.getForeignKeys(schemaName, row.TABLE_NAME);
|
|
1198
|
+
const primaryKey = indexes.find((idx) => idx.isPrimary);
|
|
1199
|
+
result.push({
|
|
1200
|
+
schema: schemaName,
|
|
1201
|
+
name: row.TABLE_NAME,
|
|
1202
|
+
type: row.TABLE_TYPE === "VIEW" ? "view" : "table",
|
|
1203
|
+
columns,
|
|
1204
|
+
primaryKey,
|
|
1205
|
+
indexes: indexes.filter((idx) => !idx.isPrimary),
|
|
1206
|
+
foreignKeys
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
async getColumns(schemaName, tableName) {
|
|
1212
|
+
const query = `
|
|
1213
|
+
SELECT
|
|
1214
|
+
COLUMN_NAME,
|
|
1215
|
+
DATA_TYPE,
|
|
1216
|
+
IS_NULLABLE,
|
|
1217
|
+
COLUMN_DEFAULT,
|
|
1218
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
1219
|
+
NUMERIC_PRECISION,
|
|
1220
|
+
NUMERIC_SCALE,
|
|
1221
|
+
COLUMNPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), COLUMN_NAME, 'IsIdentity') AS IS_IDENTITY
|
|
1222
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
1223
|
+
WHERE TABLE_SCHEMA = '${schemaName}' AND TABLE_NAME = '${tableName}'
|
|
1224
|
+
ORDER BY ORDINAL_POSITION
|
|
1225
|
+
`;
|
|
1226
|
+
const result = await this.executeQuery(query);
|
|
1227
|
+
return result.map((row) => ({
|
|
1228
|
+
name: row.COLUMN_NAME,
|
|
1229
|
+
dataType: row.DATA_TYPE,
|
|
1230
|
+
nullable: row.IS_NULLABLE === "YES",
|
|
1231
|
+
defaultValue: row.COLUMN_DEFAULT,
|
|
1232
|
+
maxLength: row.CHARACTER_MAXIMUM_LENGTH,
|
|
1233
|
+
precision: row.NUMERIC_PRECISION,
|
|
1234
|
+
scale: row.NUMERIC_SCALE,
|
|
1235
|
+
isAutoIncrement: row.IS_IDENTITY === 1
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
async getIndexes(schemaName, tableName) {
|
|
1239
|
+
const query = `
|
|
1240
|
+
SELECT
|
|
1241
|
+
i.name AS index_name,
|
|
1242
|
+
i.is_unique,
|
|
1243
|
+
i.is_primary_key,
|
|
1244
|
+
STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS column_names
|
|
1245
|
+
FROM sys.indexes i
|
|
1246
|
+
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
1247
|
+
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
1248
|
+
INNER JOIN sys.tables t ON i.object_id = t.object_id
|
|
1249
|
+
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
1250
|
+
WHERE s.name = '${schemaName}' AND t.name = '${tableName}'
|
|
1251
|
+
GROUP BY i.name, i.is_unique, i.is_primary_key
|
|
1252
|
+
`;
|
|
1253
|
+
const result = await this.executeQuery(query);
|
|
1254
|
+
return result.map((row) => ({
|
|
1255
|
+
name: row.index_name,
|
|
1256
|
+
columns: row.column_names.split(","),
|
|
1257
|
+
isUnique: row.is_unique,
|
|
1258
|
+
isPrimary: row.is_primary_key
|
|
1259
|
+
}));
|
|
1260
|
+
}
|
|
1261
|
+
async getForeignKeys(schemaName, tableName) {
|
|
1262
|
+
const query = `
|
|
1263
|
+
SELECT
|
|
1264
|
+
fk.name AS constraint_name,
|
|
1265
|
+
STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS column_names,
|
|
1266
|
+
rs.name AS referenced_schema,
|
|
1267
|
+
rt.name AS referenced_table,
|
|
1268
|
+
STRING_AGG(rc.name, ',') WITHIN GROUP (ORDER BY fkc.constraint_column_id) AS referenced_columns,
|
|
1269
|
+
fk.update_referential_action_desc,
|
|
1270
|
+
fk.delete_referential_action_desc
|
|
1271
|
+
FROM sys.foreign_keys fk
|
|
1272
|
+
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
|
1273
|
+
INNER JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id
|
|
1274
|
+
INNER JOIN sys.columns rc ON fkc.referenced_object_id = rc.object_id AND fkc.referenced_column_id = rc.column_id
|
|
1275
|
+
INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id
|
|
1276
|
+
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
1277
|
+
INNER JOIN sys.tables rt ON fk.referenced_object_id = rt.object_id
|
|
1278
|
+
INNER JOIN sys.schemas rs ON rt.schema_id = rs.schema_id
|
|
1279
|
+
WHERE s.name = '${schemaName}' AND t.name = '${tableName}'
|
|
1280
|
+
GROUP BY fk.name, rs.name, rt.name, fk.update_referential_action_desc, fk.delete_referential_action_desc
|
|
1281
|
+
`;
|
|
1282
|
+
const result = await this.executeQuery(query);
|
|
1283
|
+
return result.map((row) => ({
|
|
1284
|
+
name: row.constraint_name,
|
|
1285
|
+
columns: row.column_names.split(","),
|
|
1286
|
+
referencedSchema: row.referenced_schema,
|
|
1287
|
+
referencedTable: row.referenced_table,
|
|
1288
|
+
referencedColumns: row.referenced_columns.split(","),
|
|
1289
|
+
onUpdate: row.update_referential_action_desc,
|
|
1290
|
+
onDelete: row.delete_referential_action_desc
|
|
1291
|
+
}));
|
|
1292
|
+
}
|
|
1293
|
+
async query(sql, params = [], _timeoutMs) {
|
|
1294
|
+
this.ensureConnected();
|
|
1295
|
+
const startTime = Date.now();
|
|
1296
|
+
try {
|
|
1297
|
+
const rows = await this.executeQuery(sql, params);
|
|
1298
|
+
const executionTimeMs = Date.now() - startTime;
|
|
1299
|
+
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
1300
|
+
return {
|
|
1301
|
+
rows,
|
|
1302
|
+
columns,
|
|
1303
|
+
rowCount: rows.length,
|
|
1304
|
+
executionTimeMs
|
|
1305
|
+
};
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
this.handleError(error, "query");
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async explain(sql, params = []) {
|
|
1311
|
+
this.ensureConnected();
|
|
1312
|
+
try {
|
|
1313
|
+
const explainSql = `SET SHOWPLAN_TEXT ON; ${sql}; SET SHOWPLAN_TEXT OFF;`;
|
|
1314
|
+
const plan = await this.executeQuery(explainSql, params);
|
|
1315
|
+
return {
|
|
1316
|
+
plan,
|
|
1317
|
+
formattedPlan: JSON.stringify(plan, null, 2)
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
this.handleError(error, "explain");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async testConnection() {
|
|
1324
|
+
try {
|
|
1325
|
+
if (!this.connection) return false;
|
|
1326
|
+
await this.executeQuery("SELECT 1");
|
|
1327
|
+
return true;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async getVersion() {
|
|
1333
|
+
this.ensureConnected();
|
|
1334
|
+
try {
|
|
1335
|
+
const result = await this.executeQuery("SELECT @@VERSION as version");
|
|
1336
|
+
return result[0].version;
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
this.handleError(error, "getVersion");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/adapters/oracle.ts
|
|
1344
|
+
var OracleAdapter = class extends BaseAdapter {
|
|
1345
|
+
connection;
|
|
1346
|
+
async connect() {
|
|
1347
|
+
this.logger.warn(
|
|
1348
|
+
{ dbId: this._config.id },
|
|
1349
|
+
"Oracle adapter is not fully implemented. Requires Oracle Instant Client and oracledb package."
|
|
1350
|
+
);
|
|
1351
|
+
throw new Error(
|
|
1352
|
+
"Oracle adapter not implemented. Please install Oracle Instant Client and implement OracleAdapter methods."
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
async disconnect() {
|
|
1356
|
+
if (this.connection) {
|
|
1357
|
+
this.connection = void 0;
|
|
1358
|
+
this.connected = false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async introspect(_options) {
|
|
1362
|
+
this.ensureConnected();
|
|
1363
|
+
throw new Error("Oracle introspection not implemented");
|
|
1364
|
+
}
|
|
1365
|
+
async query(_sql, _params = [], _timeoutMs) {
|
|
1366
|
+
this.ensureConnected();
|
|
1367
|
+
throw new Error("Oracle query not implemented");
|
|
1368
|
+
}
|
|
1369
|
+
async explain(_sql, _params = []) {
|
|
1370
|
+
this.ensureConnected();
|
|
1371
|
+
throw new Error("Oracle explain not implemented");
|
|
1372
|
+
}
|
|
1373
|
+
async testConnection() {
|
|
1374
|
+
return false;
|
|
1375
|
+
}
|
|
1376
|
+
async getVersion() {
|
|
1377
|
+
return "Oracle (not implemented)";
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
|
|
1381
|
+
// src/adapters/index.ts
|
|
1382
|
+
function createAdapter(config) {
|
|
1383
|
+
switch (config.type) {
|
|
1384
|
+
case "postgres":
|
|
1385
|
+
return new PostgresAdapter(config);
|
|
1386
|
+
case "mysql":
|
|
1387
|
+
return new MySQLAdapter(config);
|
|
1388
|
+
case "sqlite":
|
|
1389
|
+
return new SQLiteAdapter(config);
|
|
1390
|
+
case "mssql":
|
|
1391
|
+
return new MSSQLAdapter(config);
|
|
1392
|
+
case "oracle":
|
|
1393
|
+
return new OracleAdapter(config);
|
|
1394
|
+
default:
|
|
1395
|
+
throw new Error(`Unsupported database type: ${config.type}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/cache.ts
|
|
1400
|
+
import fs2 from "fs/promises";
|
|
1401
|
+
import path from "path";
|
|
1402
|
+
var SchemaCache = class {
|
|
1403
|
+
constructor(_cacheDir, _defaultTtlMinutes) {
|
|
1404
|
+
this._cacheDir = _cacheDir;
|
|
1405
|
+
this._defaultTtlMinutes = _defaultTtlMinutes;
|
|
1406
|
+
}
|
|
1407
|
+
logger = getLogger();
|
|
1408
|
+
cache = /* @__PURE__ */ new Map();
|
|
1409
|
+
introspectionLocks = /* @__PURE__ */ new Map();
|
|
1410
|
+
async init() {
|
|
1411
|
+
try {
|
|
1412
|
+
await fs2.mkdir(this._cacheDir, { recursive: true });
|
|
1413
|
+
this.logger.info({ cacheDir: this._cacheDir }, "Schema cache initialized");
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
throw new CacheError("Failed to initialize cache directory", error);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Get cached schema if valid, otherwise return null
|
|
1420
|
+
*/
|
|
1421
|
+
async get(dbId) {
|
|
1422
|
+
const memEntry = this.cache.get(dbId);
|
|
1423
|
+
if (memEntry && !this.isExpired(memEntry)) {
|
|
1424
|
+
return memEntry;
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
const diskEntry = await this.loadFromDisk(dbId);
|
|
1428
|
+
if (diskEntry && !this.isExpired(diskEntry)) {
|
|
1429
|
+
this.cache.set(dbId, diskEntry);
|
|
1430
|
+
return diskEntry;
|
|
1431
|
+
}
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
this.logger.warn({ dbId, error }, "Failed to load cache from disk");
|
|
1434
|
+
}
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Set or update cache entry
|
|
1439
|
+
*/
|
|
1440
|
+
async set(dbId, schema, ttlMinutes) {
|
|
1441
|
+
const entry = {
|
|
1442
|
+
schema,
|
|
1443
|
+
relationships: this.buildRelationships(schema),
|
|
1444
|
+
cachedAt: /* @__PURE__ */ new Date(),
|
|
1445
|
+
ttlMinutes: ttlMinutes || this._defaultTtlMinutes
|
|
1446
|
+
};
|
|
1447
|
+
this.cache.set(dbId, entry);
|
|
1448
|
+
this.saveToDisk(dbId, entry).catch((error) => {
|
|
1449
|
+
this.logger.error({ dbId, error }, "Failed to save cache to disk");
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Clear cache for a specific database or all databases
|
|
1454
|
+
*/
|
|
1455
|
+
async clear(dbId) {
|
|
1456
|
+
if (dbId) {
|
|
1457
|
+
this.cache.delete(dbId);
|
|
1458
|
+
try {
|
|
1459
|
+
const filePath = this.getCacheFilePath(dbId);
|
|
1460
|
+
await fs2.unlink(filePath);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
if (error.code !== "ENOENT") {
|
|
1463
|
+
this.logger.warn({ dbId, error }, "Failed to delete cache file");
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
this.logger.info({ dbId }, "Cache cleared");
|
|
1467
|
+
} else {
|
|
1468
|
+
this.cache.clear();
|
|
1469
|
+
try {
|
|
1470
|
+
const files = await fs2.readdir(this._cacheDir);
|
|
1471
|
+
await Promise.all(
|
|
1472
|
+
files.filter((f) => f.endsWith(".json")).map((f) => fs2.unlink(path.join(this._cacheDir, f)))
|
|
1473
|
+
);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
this.logger.warn({ error }, "Failed to clear cache directory");
|
|
1476
|
+
}
|
|
1477
|
+
this.logger.info("All caches cleared");
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Get cache status
|
|
1482
|
+
*/
|
|
1483
|
+
async getStatus(dbId) {
|
|
1484
|
+
const statuses = [];
|
|
1485
|
+
if (dbId) {
|
|
1486
|
+
const status = await this.getStatusForDb(dbId);
|
|
1487
|
+
statuses.push(status);
|
|
1488
|
+
} else {
|
|
1489
|
+
const dbIds = /* @__PURE__ */ new Set([
|
|
1490
|
+
...this.cache.keys(),
|
|
1491
|
+
...await this.getPersistedDbIds()
|
|
1492
|
+
]);
|
|
1493
|
+
for (const id of dbIds) {
|
|
1494
|
+
const status = await this.getStatusForDb(id);
|
|
1495
|
+
statuses.push(status);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return statuses;
|
|
1499
|
+
}
|
|
1500
|
+
async getStatusForDb(dbId) {
|
|
1501
|
+
const entry = await this.get(dbId);
|
|
1502
|
+
if (!entry) {
|
|
1503
|
+
return {
|
|
1504
|
+
dbId,
|
|
1505
|
+
exists: false
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
const age = Date.now() - new Date(entry.cachedAt).getTime();
|
|
1509
|
+
const expired = this.isExpired(entry);
|
|
1510
|
+
return {
|
|
1511
|
+
dbId,
|
|
1512
|
+
exists: true,
|
|
1513
|
+
age,
|
|
1514
|
+
ttlMinutes: entry.ttlMinutes,
|
|
1515
|
+
expired,
|
|
1516
|
+
version: entry.schema.version,
|
|
1517
|
+
tableCount: entry.schema.schemas.reduce((sum, s) => sum + s.tables.length, 0),
|
|
1518
|
+
relationshipCount: entry.relationships.length
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Acquire lock for introspection to prevent concurrent introspection
|
|
1523
|
+
*/
|
|
1524
|
+
async acquireIntrospectionLock(dbId) {
|
|
1525
|
+
const existingLock = this.introspectionLocks.get(dbId);
|
|
1526
|
+
if (existingLock) {
|
|
1527
|
+
await existingLock;
|
|
1528
|
+
}
|
|
1529
|
+
let releaseLock;
|
|
1530
|
+
const lockPromise = new Promise((resolve2) => {
|
|
1531
|
+
releaseLock = resolve2;
|
|
1532
|
+
});
|
|
1533
|
+
this.introspectionLocks.set(dbId, lockPromise);
|
|
1534
|
+
return () => {
|
|
1535
|
+
releaseLock();
|
|
1536
|
+
this.introspectionLocks.delete(dbId);
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
isExpired(entry) {
|
|
1540
|
+
const age = Date.now() - new Date(entry.cachedAt).getTime();
|
|
1541
|
+
const ttlMs = entry.ttlMinutes * 60 * 1e3;
|
|
1542
|
+
return age > ttlMs;
|
|
1543
|
+
}
|
|
1544
|
+
buildRelationships(schema) {
|
|
1545
|
+
const relationships = [];
|
|
1546
|
+
for (const schemaObj of schema.schemas) {
|
|
1547
|
+
for (const table of schemaObj.tables) {
|
|
1548
|
+
for (const fk of table.foreignKeys) {
|
|
1549
|
+
relationships.push({
|
|
1550
|
+
fromSchema: schemaObj.name,
|
|
1551
|
+
fromTable: table.name,
|
|
1552
|
+
fromColumns: fk.columns,
|
|
1553
|
+
toSchema: fk.referencedSchema,
|
|
1554
|
+
toTable: fk.referencedTable,
|
|
1555
|
+
toColumns: fk.referencedColumns,
|
|
1556
|
+
type: "foreign_key"
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const inferred = inferRelationships(schema);
|
|
1562
|
+
const relationshipKeys = new Set(
|
|
1563
|
+
relationships.map((r) => this.getRelationshipKey(r))
|
|
1564
|
+
);
|
|
1565
|
+
for (const rel of inferred) {
|
|
1566
|
+
const key = this.getRelationshipKey(rel);
|
|
1567
|
+
if (!relationshipKeys.has(key)) {
|
|
1568
|
+
relationships.push(rel);
|
|
1569
|
+
relationshipKeys.add(key);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return relationships;
|
|
1573
|
+
}
|
|
1574
|
+
getRelationshipKey(rel) {
|
|
1575
|
+
return `${rel.fromSchema}.${rel.fromTable}.${rel.fromColumns.join(",")}\u2192${rel.toSchema}.${rel.toTable}.${rel.toColumns.join(",")}`;
|
|
1576
|
+
}
|
|
1577
|
+
getCacheFilePath(dbId) {
|
|
1578
|
+
return path.join(this._cacheDir, `${dbId}.json`);
|
|
1579
|
+
}
|
|
1580
|
+
async loadFromDisk(dbId) {
|
|
1581
|
+
try {
|
|
1582
|
+
const filePath = this.getCacheFilePath(dbId);
|
|
1583
|
+
const data = await fs2.readFile(filePath, "utf-8");
|
|
1584
|
+
const entry = JSON.parse(data);
|
|
1585
|
+
entry.cachedAt = new Date(entry.cachedAt);
|
|
1586
|
+
entry.schema.introspectedAt = new Date(entry.schema.introspectedAt);
|
|
1587
|
+
return entry;
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (error.code === "ENOENT") {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
throw error;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async saveToDisk(dbId, entry) {
|
|
1596
|
+
const filePath = this.getCacheFilePath(dbId);
|
|
1597
|
+
const data = JSON.stringify(entry, null, 2);
|
|
1598
|
+
await fs2.writeFile(filePath, data, "utf-8");
|
|
1599
|
+
}
|
|
1600
|
+
async getPersistedDbIds() {
|
|
1601
|
+
try {
|
|
1602
|
+
const files = await fs2.readdir(this._cacheDir);
|
|
1603
|
+
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
|
|
1604
|
+
} catch {
|
|
1605
|
+
return [];
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// src/query-tracker.ts
|
|
1611
|
+
var QueryTracker = class {
|
|
1612
|
+
history = /* @__PURE__ */ new Map();
|
|
1613
|
+
maxHistoryPerDb = 100;
|
|
1614
|
+
track(dbId, sql, executionTimeMs, rowCount, error) {
|
|
1615
|
+
const entry = {
|
|
1616
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1617
|
+
sql,
|
|
1618
|
+
tables: extractTableNames(sql),
|
|
1619
|
+
executionTimeMs,
|
|
1620
|
+
rowCount,
|
|
1621
|
+
error
|
|
1622
|
+
};
|
|
1623
|
+
if (!this.history.has(dbId)) {
|
|
1624
|
+
this.history.set(dbId, []);
|
|
1625
|
+
}
|
|
1626
|
+
const dbHistory = this.history.get(dbId);
|
|
1627
|
+
dbHistory.push(entry);
|
|
1628
|
+
if (dbHistory.length > this.maxHistoryPerDb) {
|
|
1629
|
+
dbHistory.shift();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
getHistory(dbId, limit) {
|
|
1633
|
+
const dbHistory = this.history.get(dbId) || [];
|
|
1634
|
+
if (limit) {
|
|
1635
|
+
return dbHistory.slice(-limit);
|
|
1636
|
+
}
|
|
1637
|
+
return [...dbHistory];
|
|
1638
|
+
}
|
|
1639
|
+
getStats(dbId) {
|
|
1640
|
+
const dbHistory = this.history.get(dbId) || [];
|
|
1641
|
+
const stats = {
|
|
1642
|
+
totalQueries: dbHistory.length,
|
|
1643
|
+
avgExecutionTime: 0,
|
|
1644
|
+
errorCount: 0,
|
|
1645
|
+
tableUsage: {}
|
|
1646
|
+
};
|
|
1647
|
+
if (dbHistory.length === 0) {
|
|
1648
|
+
return stats;
|
|
1649
|
+
}
|
|
1650
|
+
let totalTime = 0;
|
|
1651
|
+
for (const entry of dbHistory) {
|
|
1652
|
+
totalTime += entry.executionTimeMs;
|
|
1653
|
+
if (entry.error) {
|
|
1654
|
+
stats.errorCount++;
|
|
1655
|
+
}
|
|
1656
|
+
for (const table of entry.tables) {
|
|
1657
|
+
stats.tableUsage[table] = (stats.tableUsage[table] || 0) + 1;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
stats.avgExecutionTime = totalTime / dbHistory.length;
|
|
1661
|
+
return stats;
|
|
1662
|
+
}
|
|
1663
|
+
clear(dbId) {
|
|
1664
|
+
if (dbId) {
|
|
1665
|
+
this.history.delete(dbId);
|
|
1666
|
+
} else {
|
|
1667
|
+
this.history.clear();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// src/database-manager.ts
|
|
1673
|
+
var DatabaseManager = class {
|
|
1674
|
+
constructor(_configs, options) {
|
|
1675
|
+
this._configs = _configs;
|
|
1676
|
+
this.options = options;
|
|
1677
|
+
this.cache = new SchemaCache(options.cacheDir, options.cacheTtlMinutes);
|
|
1678
|
+
}
|
|
1679
|
+
logger = getLogger();
|
|
1680
|
+
adapters = /* @__PURE__ */ new Map();
|
|
1681
|
+
cache;
|
|
1682
|
+
queryTracker = new QueryTracker();
|
|
1683
|
+
async init() {
|
|
1684
|
+
await this.cache.init();
|
|
1685
|
+
for (const config of this._configs) {
|
|
1686
|
+
const adapter = createAdapter(config);
|
|
1687
|
+
this.adapters.set(config.id, adapter);
|
|
1688
|
+
if (config.eagerConnect) {
|
|
1689
|
+
try {
|
|
1690
|
+
await this.connect(config.id);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
this.logger.error({ dbId: config.id, error }, "Failed to eager connect");
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
this.logger.info({ databases: this._configs.length }, "Database manager initialized");
|
|
1697
|
+
}
|
|
1698
|
+
async shutdown() {
|
|
1699
|
+
for (const [dbId, adapter] of this.adapters) {
|
|
1700
|
+
try {
|
|
1701
|
+
await adapter.disconnect();
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
this.logger.error({ dbId, error }, "Failed to disconnect");
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
this.logger.info("Database manager shut down");
|
|
1707
|
+
}
|
|
1708
|
+
getConfigs() {
|
|
1709
|
+
return this._configs;
|
|
1710
|
+
}
|
|
1711
|
+
getConfig(dbId) {
|
|
1712
|
+
return this._configs.find((c) => c.id === dbId);
|
|
1713
|
+
}
|
|
1714
|
+
getAdapter(dbId) {
|
|
1715
|
+
const adapter = this.adapters.get(dbId);
|
|
1716
|
+
if (!adapter) {
|
|
1717
|
+
throw new Error(`Database not found: ${dbId}`);
|
|
1718
|
+
}
|
|
1719
|
+
return adapter;
|
|
1720
|
+
}
|
|
1721
|
+
async connect(dbId) {
|
|
1722
|
+
const adapter = this.getAdapter(dbId);
|
|
1723
|
+
await adapter.connect();
|
|
1724
|
+
}
|
|
1725
|
+
async ensureConnected(dbId) {
|
|
1726
|
+
const adapter = this.getAdapter(dbId);
|
|
1727
|
+
const connected = await adapter.testConnection();
|
|
1728
|
+
if (!connected) {
|
|
1729
|
+
await this.connect(dbId);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
async testConnection(dbId) {
|
|
1733
|
+
const adapter = this.getAdapter(dbId);
|
|
1734
|
+
return adapter.testConnection();
|
|
1735
|
+
}
|
|
1736
|
+
async getVersion(dbId) {
|
|
1737
|
+
await this.ensureConnected(dbId);
|
|
1738
|
+
const adapter = this.getAdapter(dbId);
|
|
1739
|
+
return adapter.getVersion();
|
|
1740
|
+
}
|
|
1741
|
+
async introspectSchema(dbId, forceRefresh = false, options) {
|
|
1742
|
+
if (!forceRefresh) {
|
|
1743
|
+
const cached = await this.cache.get(dbId);
|
|
1744
|
+
if (cached) {
|
|
1745
|
+
this.logger.debug({ dbId }, "Using cached schema");
|
|
1746
|
+
return cached;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const releaseLock = await this.cache.acquireIntrospectionLock(dbId);
|
|
1750
|
+
try {
|
|
1751
|
+
if (!forceRefresh) {
|
|
1752
|
+
const cached = await this.cache.get(dbId);
|
|
1753
|
+
if (cached) {
|
|
1754
|
+
return cached;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
this.logger.info({ dbId, forceRefresh }, "Introspecting schema");
|
|
1758
|
+
await this.ensureConnected(dbId);
|
|
1759
|
+
const adapter = this.getAdapter(dbId);
|
|
1760
|
+
const schema = await adapter.introspect(options);
|
|
1761
|
+
const config = this.getConfig(dbId);
|
|
1762
|
+
await this.cache.set(dbId, schema, config?.introspection?.maxTables);
|
|
1763
|
+
const entry = await this.cache.get(dbId);
|
|
1764
|
+
return entry;
|
|
1765
|
+
} finally {
|
|
1766
|
+
releaseLock();
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
async getSchema(dbId) {
|
|
1770
|
+
return this.introspectSchema(dbId, false);
|
|
1771
|
+
}
|
|
1772
|
+
async runQuery(dbId, sql, params = [], timeoutMs) {
|
|
1773
|
+
const config = this.getConfig(dbId);
|
|
1774
|
+
if (isWriteOperation(sql)) {
|
|
1775
|
+
if (!this.options.allowWrite && !config?.readOnly === false) {
|
|
1776
|
+
throw new Error("Write operations are not allowed. Set allowWrite in config.");
|
|
1777
|
+
}
|
|
1778
|
+
if (this.options.disableDangerousOperations) {
|
|
1779
|
+
const operation = sql.trim().split(/\s+/)[0].toUpperCase();
|
|
1780
|
+
const dangerousOps = ["DELETE", "TRUNCATE", "DROP"];
|
|
1781
|
+
if (dangerousOps.includes(operation)) {
|
|
1782
|
+
throw new Error(`Dangerous operation ${operation} is disabled. Set disableDangerousOperations: false in security config to allow.`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (this.options.allowedWriteOperations && this.options.allowedWriteOperations.length > 0) {
|
|
1786
|
+
const operation = sql.trim().split(/\s+/)[0].toUpperCase();
|
|
1787
|
+
if (!this.options.allowedWriteOperations.includes(operation)) {
|
|
1788
|
+
throw new Error(`Write operation ${operation} is not allowed.`);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
await this.introspectSchema(dbId, false);
|
|
1793
|
+
await this.ensureConnected(dbId);
|
|
1794
|
+
const adapter = this.getAdapter(dbId);
|
|
1795
|
+
try {
|
|
1796
|
+
const result = await adapter.query(sql, params, timeoutMs);
|
|
1797
|
+
this.queryTracker.track(dbId, sql, result.executionTimeMs, result.rowCount);
|
|
1798
|
+
return result;
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
this.queryTracker.track(dbId, sql, 0, 0, error.message);
|
|
1801
|
+
throw error;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
async explainQuery(dbId, sql, params = []) {
|
|
1805
|
+
await this.ensureConnected(dbId);
|
|
1806
|
+
const adapter = this.getAdapter(dbId);
|
|
1807
|
+
return adapter.explain(sql, params);
|
|
1808
|
+
}
|
|
1809
|
+
async suggestJoins(dbId, tables) {
|
|
1810
|
+
const cacheEntry = await this.getSchema(dbId);
|
|
1811
|
+
return findJoinPaths(tables, cacheEntry.relationships);
|
|
1812
|
+
}
|
|
1813
|
+
async clearCache(dbId) {
|
|
1814
|
+
await this.cache.clear(dbId);
|
|
1815
|
+
this.queryTracker.clear(dbId);
|
|
1816
|
+
}
|
|
1817
|
+
async getCacheStatus(dbId) {
|
|
1818
|
+
return this.cache.getStatus(dbId);
|
|
1819
|
+
}
|
|
1820
|
+
getQueryStats(dbId) {
|
|
1821
|
+
return this.queryTracker.getStats(dbId);
|
|
1822
|
+
}
|
|
1823
|
+
getQueryHistory(dbId, limit) {
|
|
1824
|
+
return this.queryTracker.getHistory(dbId, limit);
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
|
|
1828
|
+
// src/mcp-server.ts
|
|
1829
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1830
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1831
|
+
import {
|
|
1832
|
+
CallToolRequestSchema,
|
|
1833
|
+
ListToolsRequestSchema,
|
|
1834
|
+
ListResourcesRequestSchema,
|
|
1835
|
+
ReadResourceRequestSchema
|
|
1836
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1837
|
+
var MCPServer = class {
|
|
1838
|
+
constructor(_dbManager, _config) {
|
|
1839
|
+
this._dbManager = _dbManager;
|
|
1840
|
+
this._config = _config;
|
|
1841
|
+
this.server = new Server(
|
|
1842
|
+
{
|
|
1843
|
+
name: "mcp-database-server",
|
|
1844
|
+
version: "1.0.0"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
capabilities: {
|
|
1848
|
+
tools: {},
|
|
1849
|
+
resources: {}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1853
|
+
this.setupHandlers();
|
|
1854
|
+
}
|
|
1855
|
+
server;
|
|
1856
|
+
logger = getLogger();
|
|
1857
|
+
setupHandlers() {
|
|
1858
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1859
|
+
tools: [
|
|
1860
|
+
{
|
|
1861
|
+
name: "list_databases",
|
|
1862
|
+
description: "List all configured databases with their status",
|
|
1863
|
+
inputSchema: {
|
|
1864
|
+
type: "object",
|
|
1865
|
+
properties: {}
|
|
1866
|
+
}
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
name: "introspect_schema",
|
|
1870
|
+
description: "Introspect database schema and cache it",
|
|
1871
|
+
inputSchema: {
|
|
1872
|
+
type: "object",
|
|
1873
|
+
properties: {
|
|
1874
|
+
dbId: {
|
|
1875
|
+
type: "string",
|
|
1876
|
+
description: "Database ID to introspect"
|
|
1877
|
+
},
|
|
1878
|
+
forceRefresh: {
|
|
1879
|
+
type: "boolean",
|
|
1880
|
+
description: "Force refresh even if cached",
|
|
1881
|
+
default: false
|
|
1882
|
+
},
|
|
1883
|
+
schemaFilter: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
description: "Optional schema filtering options",
|
|
1886
|
+
properties: {
|
|
1887
|
+
includeSchemas: {
|
|
1888
|
+
type: "array",
|
|
1889
|
+
items: { type: "string" }
|
|
1890
|
+
},
|
|
1891
|
+
excludeSchemas: {
|
|
1892
|
+
type: "array",
|
|
1893
|
+
items: { type: "string" }
|
|
1894
|
+
},
|
|
1895
|
+
includeViews: { type: "boolean" },
|
|
1896
|
+
maxTables: { type: "number" }
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
},
|
|
1900
|
+
required: ["dbId"]
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
{
|
|
1904
|
+
name: "get_schema",
|
|
1905
|
+
description: "Get cached schema metadata",
|
|
1906
|
+
inputSchema: {
|
|
1907
|
+
type: "object",
|
|
1908
|
+
properties: {
|
|
1909
|
+
dbId: {
|
|
1910
|
+
type: "string",
|
|
1911
|
+
description: "Database ID"
|
|
1912
|
+
},
|
|
1913
|
+
schema: {
|
|
1914
|
+
type: "string",
|
|
1915
|
+
description: "Optional schema name to filter"
|
|
1916
|
+
},
|
|
1917
|
+
table: {
|
|
1918
|
+
type: "string",
|
|
1919
|
+
description: "Optional table name to filter"
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1922
|
+
required: ["dbId"]
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
name: "run_query",
|
|
1927
|
+
description: "Execute SQL query against a database",
|
|
1928
|
+
inputSchema: {
|
|
1929
|
+
type: "object",
|
|
1930
|
+
properties: {
|
|
1931
|
+
dbId: {
|
|
1932
|
+
type: "string",
|
|
1933
|
+
description: "Database ID"
|
|
1934
|
+
},
|
|
1935
|
+
sql: {
|
|
1936
|
+
type: "string",
|
|
1937
|
+
description: "SQL query to execute"
|
|
1938
|
+
},
|
|
1939
|
+
params: {
|
|
1940
|
+
type: "array",
|
|
1941
|
+
description: "Query parameters",
|
|
1942
|
+
items: {}
|
|
1943
|
+
},
|
|
1944
|
+
limit: {
|
|
1945
|
+
type: "number",
|
|
1946
|
+
description: "Maximum number of rows to return"
|
|
1947
|
+
},
|
|
1948
|
+
timeoutMs: {
|
|
1949
|
+
type: "number",
|
|
1950
|
+
description: "Query timeout in milliseconds"
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
required: ["dbId", "sql"]
|
|
1954
|
+
}
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
name: "explain_query",
|
|
1958
|
+
description: "Get query execution plan",
|
|
1959
|
+
inputSchema: {
|
|
1960
|
+
type: "object",
|
|
1961
|
+
properties: {
|
|
1962
|
+
dbId: {
|
|
1963
|
+
type: "string",
|
|
1964
|
+
description: "Database ID"
|
|
1965
|
+
},
|
|
1966
|
+
sql: {
|
|
1967
|
+
type: "string",
|
|
1968
|
+
description: "SQL query to explain"
|
|
1969
|
+
},
|
|
1970
|
+
params: {
|
|
1971
|
+
type: "array",
|
|
1972
|
+
description: "Query parameters",
|
|
1973
|
+
items: {}
|
|
1974
|
+
}
|
|
1975
|
+
},
|
|
1976
|
+
required: ["dbId", "sql"]
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
{
|
|
1980
|
+
name: "suggest_joins",
|
|
1981
|
+
description: "Suggest join paths between tables based on relationships",
|
|
1982
|
+
inputSchema: {
|
|
1983
|
+
type: "object",
|
|
1984
|
+
properties: {
|
|
1985
|
+
dbId: {
|
|
1986
|
+
type: "string",
|
|
1987
|
+
description: "Database ID"
|
|
1988
|
+
},
|
|
1989
|
+
tables: {
|
|
1990
|
+
type: "array",
|
|
1991
|
+
description: "List of table names to join",
|
|
1992
|
+
items: { type: "string" },
|
|
1993
|
+
minItems: 2
|
|
1994
|
+
}
|
|
1995
|
+
},
|
|
1996
|
+
required: ["dbId", "tables"]
|
|
1997
|
+
}
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
name: "clear_cache",
|
|
2001
|
+
description: "Clear schema cache",
|
|
2002
|
+
inputSchema: {
|
|
2003
|
+
type: "object",
|
|
2004
|
+
properties: {
|
|
2005
|
+
dbId: {
|
|
2006
|
+
type: "string",
|
|
2007
|
+
description: "Optional database ID (clears all if omitted)"
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
name: "cache_status",
|
|
2014
|
+
description: "Get cache status and statistics",
|
|
2015
|
+
inputSchema: {
|
|
2016
|
+
type: "object",
|
|
2017
|
+
properties: {
|
|
2018
|
+
dbId: {
|
|
2019
|
+
type: "string",
|
|
2020
|
+
description: "Optional database ID"
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
name: "health_check",
|
|
2027
|
+
description: "Check database connectivity and get version info",
|
|
2028
|
+
inputSchema: {
|
|
2029
|
+
type: "object",
|
|
2030
|
+
properties: {
|
|
2031
|
+
dbId: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
description: "Optional database ID (checks all if omitted)"
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
]
|
|
2039
|
+
}));
|
|
2040
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2041
|
+
const { name, arguments: args } = request.params;
|
|
2042
|
+
try {
|
|
2043
|
+
switch (name) {
|
|
2044
|
+
case "list_databases":
|
|
2045
|
+
return await this.handleListDatabases();
|
|
2046
|
+
case "introspect_schema":
|
|
2047
|
+
return await this.handleIntrospectSchema(args);
|
|
2048
|
+
case "get_schema":
|
|
2049
|
+
return await this.handleGetSchema(args);
|
|
2050
|
+
case "run_query":
|
|
2051
|
+
return await this.handleRunQuery(args);
|
|
2052
|
+
case "explain_query":
|
|
2053
|
+
return await this.handleExplainQuery(args);
|
|
2054
|
+
case "suggest_joins":
|
|
2055
|
+
return await this.handleSuggestJoins(args);
|
|
2056
|
+
case "clear_cache":
|
|
2057
|
+
return await this.handleClearCache(args);
|
|
2058
|
+
case "cache_status":
|
|
2059
|
+
return await this.handleCacheStatus(args);
|
|
2060
|
+
case "health_check":
|
|
2061
|
+
return await this.handleHealthCheck(args);
|
|
2062
|
+
default:
|
|
2063
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
2064
|
+
}
|
|
2065
|
+
} catch (error) {
|
|
2066
|
+
this.logger.error({ tool: name, error }, "Tool execution failed");
|
|
2067
|
+
return {
|
|
2068
|
+
content: [
|
|
2069
|
+
{
|
|
2070
|
+
type: "text",
|
|
2071
|
+
text: JSON.stringify({
|
|
2072
|
+
error: error.message,
|
|
2073
|
+
code: error.code || "TOOL_ERROR"
|
|
2074
|
+
})
|
|
2075
|
+
}
|
|
2076
|
+
]
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2080
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2081
|
+
const statuses = await this._dbManager.getCacheStatus();
|
|
2082
|
+
const resources = statuses.filter((s) => s.exists).map((s) => ({
|
|
2083
|
+
uri: `schema://${s.dbId}`,
|
|
2084
|
+
name: `Schema: ${s.dbId}`,
|
|
2085
|
+
description: `Cached schema for ${s.dbId} (${s.tableCount} tables, ${s.relationshipCount} relationships)`,
|
|
2086
|
+
mimeType: "application/json"
|
|
2087
|
+
}));
|
|
2088
|
+
return { resources };
|
|
2089
|
+
});
|
|
2090
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
2091
|
+
const uri = request.params.uri;
|
|
2092
|
+
const match = uri.match(/^schema:\/\/(.+)$/);
|
|
2093
|
+
if (!match) {
|
|
2094
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
2095
|
+
}
|
|
2096
|
+
const dbId = match[1];
|
|
2097
|
+
const cacheEntry = await this._dbManager.getSchema(dbId);
|
|
2098
|
+
return {
|
|
2099
|
+
contents: [
|
|
2100
|
+
{
|
|
2101
|
+
uri,
|
|
2102
|
+
mimeType: "application/json",
|
|
2103
|
+
text: JSON.stringify(cacheEntry, null, 2)
|
|
2104
|
+
}
|
|
2105
|
+
]
|
|
2106
|
+
};
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
async handleListDatabases() {
|
|
2110
|
+
const configs = this._dbManager.getConfigs();
|
|
2111
|
+
const statuses = await Promise.all(
|
|
2112
|
+
configs.map(async (config) => {
|
|
2113
|
+
const connected = await this._dbManager.testConnection(config.id);
|
|
2114
|
+
const cacheStatus = (await this._dbManager.getCacheStatus(config.id))[0];
|
|
2115
|
+
return {
|
|
2116
|
+
id: config.id,
|
|
2117
|
+
type: config.type,
|
|
2118
|
+
url: this._config.security?.redactSecrets ? redactUrl(config.url || "") : config.url,
|
|
2119
|
+
connected,
|
|
2120
|
+
cached: cacheStatus?.exists || false,
|
|
2121
|
+
cacheAge: cacheStatus?.age,
|
|
2122
|
+
version: cacheStatus?.version
|
|
2123
|
+
};
|
|
2124
|
+
})
|
|
2125
|
+
);
|
|
2126
|
+
return {
|
|
2127
|
+
content: [
|
|
2128
|
+
{
|
|
2129
|
+
type: "text",
|
|
2130
|
+
text: JSON.stringify(statuses, null, 2)
|
|
2131
|
+
}
|
|
2132
|
+
]
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
async handleIntrospectSchema(args) {
|
|
2136
|
+
const result = await this._dbManager.introspectSchema(
|
|
2137
|
+
args.dbId,
|
|
2138
|
+
args.forceRefresh || false,
|
|
2139
|
+
args.schemaFilter
|
|
2140
|
+
);
|
|
2141
|
+
const summary = {
|
|
2142
|
+
dbId: args.dbId,
|
|
2143
|
+
version: result.schema.version,
|
|
2144
|
+
introspectedAt: result.schema.introspectedAt,
|
|
2145
|
+
schemas: result.schema.schemas.map((s) => ({
|
|
2146
|
+
name: s.name,
|
|
2147
|
+
tableCount: s.tables.length,
|
|
2148
|
+
viewCount: s.tables.filter((t) => t.type === "view").length
|
|
2149
|
+
})),
|
|
2150
|
+
totalTables: result.schema.schemas.reduce((sum, s) => sum + s.tables.length, 0),
|
|
2151
|
+
totalRelationships: result.relationships.length
|
|
2152
|
+
};
|
|
2153
|
+
return {
|
|
2154
|
+
content: [
|
|
2155
|
+
{
|
|
2156
|
+
type: "text",
|
|
2157
|
+
text: JSON.stringify(summary, null, 2)
|
|
2158
|
+
}
|
|
2159
|
+
]
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
async handleGetSchema(args) {
|
|
2163
|
+
const cacheEntry = await this._dbManager.getSchema(args.dbId);
|
|
2164
|
+
let result = cacheEntry.schema;
|
|
2165
|
+
if (args.schema) {
|
|
2166
|
+
result = {
|
|
2167
|
+
...result,
|
|
2168
|
+
schemas: result.schemas.filter((s) => s.name === args.schema)
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
if (args.table) {
|
|
2172
|
+
result = {
|
|
2173
|
+
...result,
|
|
2174
|
+
schemas: result.schemas.map((s) => ({
|
|
2175
|
+
...s,
|
|
2176
|
+
tables: s.tables.filter((t) => t.name === args.table)
|
|
2177
|
+
}))
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
content: [
|
|
2182
|
+
{
|
|
2183
|
+
type: "text",
|
|
2184
|
+
text: JSON.stringify(result, null, 2)
|
|
2185
|
+
}
|
|
2186
|
+
]
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
async handleRunQuery(args) {
|
|
2190
|
+
let sql = args.sql;
|
|
2191
|
+
if (args.limit && !sql.toUpperCase().includes("LIMIT")) {
|
|
2192
|
+
sql += ` LIMIT ${args.limit}`;
|
|
2193
|
+
}
|
|
2194
|
+
const result = await this._dbManager.runQuery(args.dbId, sql, args.params, args.timeoutMs);
|
|
2195
|
+
const cacheEntry = await this._dbManager.getSchema(args.dbId);
|
|
2196
|
+
const queryStats = this._dbManager.getQueryStats(args.dbId);
|
|
2197
|
+
return {
|
|
2198
|
+
content: [
|
|
2199
|
+
{
|
|
2200
|
+
type: "text",
|
|
2201
|
+
text: JSON.stringify(
|
|
2202
|
+
{
|
|
2203
|
+
...result,
|
|
2204
|
+
metadata: {
|
|
2205
|
+
relationships: cacheEntry.relationships.filter(
|
|
2206
|
+
(r) => result.columns.some(
|
|
2207
|
+
(col) => col.includes(r.fromTable) || col.includes(r.toTable)
|
|
2208
|
+
)
|
|
2209
|
+
),
|
|
2210
|
+
queryStats
|
|
2211
|
+
}
|
|
2212
|
+
},
|
|
2213
|
+
null,
|
|
2214
|
+
2
|
|
2215
|
+
)
|
|
2216
|
+
}
|
|
2217
|
+
]
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
async handleExplainQuery(args) {
|
|
2221
|
+
const result = await this._dbManager.explainQuery(args.dbId, args.sql, args.params);
|
|
2222
|
+
return {
|
|
2223
|
+
content: [
|
|
2224
|
+
{
|
|
2225
|
+
type: "text",
|
|
2226
|
+
text: JSON.stringify(result, null, 2)
|
|
2227
|
+
}
|
|
2228
|
+
]
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async handleSuggestJoins(args) {
|
|
2232
|
+
const joinPaths = await this._dbManager.suggestJoins(args.dbId, args.tables);
|
|
2233
|
+
return {
|
|
2234
|
+
content: [
|
|
2235
|
+
{
|
|
2236
|
+
type: "text",
|
|
2237
|
+
text: JSON.stringify(joinPaths, null, 2)
|
|
2238
|
+
}
|
|
2239
|
+
]
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async handleClearCache(args) {
|
|
2243
|
+
await this._dbManager.clearCache(args.dbId);
|
|
2244
|
+
return {
|
|
2245
|
+
content: [
|
|
2246
|
+
{
|
|
2247
|
+
type: "text",
|
|
2248
|
+
text: JSON.stringify({
|
|
2249
|
+
success: true,
|
|
2250
|
+
message: args.dbId ? `Cache cleared for ${args.dbId}` : "All caches cleared"
|
|
2251
|
+
})
|
|
2252
|
+
}
|
|
2253
|
+
]
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
async handleCacheStatus(args) {
|
|
2257
|
+
const statuses = await this._dbManager.getCacheStatus(args.dbId);
|
|
2258
|
+
return {
|
|
2259
|
+
content: [
|
|
2260
|
+
{
|
|
2261
|
+
type: "text",
|
|
2262
|
+
text: JSON.stringify(statuses, null, 2)
|
|
2263
|
+
}
|
|
2264
|
+
]
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
async handleHealthCheck(args) {
|
|
2268
|
+
const configs = args.dbId ? [this._dbManager.getConfig(args.dbId)] : this._dbManager.getConfigs();
|
|
2269
|
+
const results = await Promise.all(
|
|
2270
|
+
configs.map(async (config) => {
|
|
2271
|
+
try {
|
|
2272
|
+
const connected = await this._dbManager.testConnection(config.id);
|
|
2273
|
+
const version2 = connected ? await this._dbManager.getVersion(config.id) : "N/A";
|
|
2274
|
+
return {
|
|
2275
|
+
dbId: config.id,
|
|
2276
|
+
healthy: connected,
|
|
2277
|
+
version: version2
|
|
2278
|
+
};
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
return {
|
|
2281
|
+
dbId: config.id,
|
|
2282
|
+
healthy: false,
|
|
2283
|
+
error: error.message
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
})
|
|
2287
|
+
);
|
|
2288
|
+
return {
|
|
2289
|
+
content: [
|
|
2290
|
+
{
|
|
2291
|
+
type: "text",
|
|
2292
|
+
text: JSON.stringify(results, null, 2)
|
|
2293
|
+
}
|
|
2294
|
+
]
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
async start() {
|
|
2298
|
+
const transport = new StdioServerTransport();
|
|
2299
|
+
await this.server.connect(transport);
|
|
2300
|
+
this.logger.info("MCP server started");
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
// src/index.ts
|
|
2305
|
+
dotenv.config();
|
|
2306
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
2307
|
+
var __dirname2 = dirname2(__filename2);
|
|
2308
|
+
var packageJson = JSON.parse(
|
|
2309
|
+
readFileSync(join2(__dirname2, "../package.json"), "utf-8")
|
|
2310
|
+
);
|
|
2311
|
+
var version = packageJson.version;
|
|
2312
|
+
async function main() {
|
|
2313
|
+
try {
|
|
2314
|
+
const { values } = parseArgs({
|
|
2315
|
+
options: {
|
|
2316
|
+
config: {
|
|
2317
|
+
type: "string",
|
|
2318
|
+
short: "c",
|
|
2319
|
+
default: "./.mcp-database-server.config"
|
|
2320
|
+
},
|
|
2321
|
+
help: {
|
|
2322
|
+
type: "boolean",
|
|
2323
|
+
short: "h"
|
|
2324
|
+
},
|
|
2325
|
+
version: {
|
|
2326
|
+
type: "boolean",
|
|
2327
|
+
short: "v"
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
if (values.version) {
|
|
2332
|
+
console.log(version);
|
|
2333
|
+
process.exit(0);
|
|
2334
|
+
}
|
|
2335
|
+
if (values.help) {
|
|
2336
|
+
console.log(`
|
|
2337
|
+
mcp-database-server - Model Context Protocol Server for SQL Databases
|
|
2338
|
+
|
|
2339
|
+
Usage:
|
|
2340
|
+
mcp-database-server [options]
|
|
2341
|
+
|
|
2342
|
+
Options:
|
|
2343
|
+
-c, --config <path> Path to configuration file (default: ./.mcp-database-server.config)
|
|
2344
|
+
-h, --help Show this help message
|
|
2345
|
+
-v, --version Show version number
|
|
2346
|
+
|
|
2347
|
+
Configuration:
|
|
2348
|
+
The config file should be a JSON file with database configurations.
|
|
2349
|
+
See mcp-database-server.config.example for reference.
|
|
2350
|
+
|
|
2351
|
+
Environment Variables:
|
|
2352
|
+
You can use environment variable interpolation in the config file:
|
|
2353
|
+
Example: "url": "\${DB_URL_POSTGRES}"
|
|
2354
|
+
|
|
2355
|
+
Examples:
|
|
2356
|
+
mcp-database-server --config ./my-config.json
|
|
2357
|
+
mcp-database-server -c ./config/production.json
|
|
2358
|
+
`);
|
|
2359
|
+
process.exit(0);
|
|
2360
|
+
}
|
|
2361
|
+
let configPath = values.config;
|
|
2362
|
+
if (configPath === "./.mcp-database-server.config") {
|
|
2363
|
+
const foundPath = findConfigFile(".mcp-database-server.config");
|
|
2364
|
+
if (foundPath) {
|
|
2365
|
+
configPath = foundPath;
|
|
2366
|
+
} else {
|
|
2367
|
+
console.error("Error: Config file .mcp-database-server.config not found");
|
|
2368
|
+
console.error("Searched in current directory and all parent directories");
|
|
2369
|
+
console.error("\nTo create a config file:");
|
|
2370
|
+
console.error(" cp mcp-database-server.config.example .mcp-database-server.config");
|
|
2371
|
+
console.error("\nOr specify a custom path:");
|
|
2372
|
+
console.error(" mcp-database-server --config /path/to/config.json");
|
|
2373
|
+
process.exit(1);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
const config = await loadConfig(configPath);
|
|
2377
|
+
initLogger(config.logging?.level || "info", config.logging?.pretty || false);
|
|
2378
|
+
const logger2 = getLogger();
|
|
2379
|
+
logger2.info({ configPath }, "Configuration loaded");
|
|
2380
|
+
const dbManager = new DatabaseManager(config.databases, {
|
|
2381
|
+
cacheDir: config.cache?.directory || ".sql-mcp-cache",
|
|
2382
|
+
cacheTtlMinutes: config.cache?.ttlMinutes || 10,
|
|
2383
|
+
allowWrite: config.security?.allowWrite || false,
|
|
2384
|
+
allowedWriteOperations: config.security?.allowedWriteOperations,
|
|
2385
|
+
disableDangerousOperations: config.security?.disableDangerousOperations ?? true
|
|
2386
|
+
});
|
|
2387
|
+
await dbManager.init();
|
|
2388
|
+
const mcpServer = new MCPServer(dbManager, config);
|
|
2389
|
+
await mcpServer.start();
|
|
2390
|
+
const shutdown = async (signal) => {
|
|
2391
|
+
logger2.info({ signal }, "Shutting down...");
|
|
2392
|
+
await dbManager.shutdown();
|
|
2393
|
+
process.exit(0);
|
|
2394
|
+
};
|
|
2395
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2396
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
console.error("Fatal error:", error.message);
|
|
2399
|
+
if (error.details) {
|
|
2400
|
+
console.error("Details:", JSON.stringify(error.details, null, 2));
|
|
2401
|
+
}
|
|
2402
|
+
process.exit(1);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
main();
|
|
2406
|
+
//# sourceMappingURL=index.js.map
|