@bytebase/dbhub 0.22.2 → 0.22.4
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/dist/{chunk-MEBWJMLY.js → chunk-DN45LGY4.js} +192 -5
- package/dist/index.js +47 -15
- package/dist/{mariadb-DVQM4MMM.js → mariadb-ERB4D3RP.js} +14 -1
- package/dist/{mysql-NCKXNVZJ.js → mysql-WG6UFALX.js} +14 -1
- package/dist/{registry-FZPB5LRL.js → registry-OF5GPCBK.js} +1 -1
- package/dist/{sqlserver-5E7DSDVX.js → sqlserver-5TFVBCMA.js} +62 -2
- package/package.json +1 -1
|
@@ -1094,6 +1094,92 @@ function validateToolsConfig(tools, sources, configPath) {
|
|
|
1094
1094
|
}
|
|
1095
1095
|
}
|
|
1096
1096
|
}
|
|
1097
|
+
function getRawDSNQueryParam(dsn, key) {
|
|
1098
|
+
const queryStart = dsn.indexOf("?");
|
|
1099
|
+
if (queryStart === -1) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
for (const pair of dsn.substring(queryStart + 1).split("&")) {
|
|
1103
|
+
if (pair === "") {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const eq = pair.indexOf("=");
|
|
1107
|
+
const rawKey = eq === -1 ? pair : pair.substring(0, eq);
|
|
1108
|
+
if (rawKey === key) {
|
|
1109
|
+
const rawValue = eq === -1 ? "" : pair.substring(eq + 1);
|
|
1110
|
+
try {
|
|
1111
|
+
return decodeURIComponent(rawValue);
|
|
1112
|
+
} catch {
|
|
1113
|
+
return rawValue;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
function validateDSNFieldConflicts(source, configPath) {
|
|
1120
|
+
const conflict = (field, fieldValue, dsnValue) => {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`Configuration file ${configPath}: source '${source.id}' has conflicting ${field}: the DSN specifies '${dsnValue}' but the ${field} field is '${fieldValue}'. Set ${field} in only one place, or make the two values match.`
|
|
1123
|
+
);
|
|
1124
|
+
};
|
|
1125
|
+
const info = parseConnectionInfoFromDSN(source.dsn);
|
|
1126
|
+
if (source.type && info?.type && source.type !== info.type) {
|
|
1127
|
+
conflict("type", source.type, info.type);
|
|
1128
|
+
}
|
|
1129
|
+
if (info?.type === "sqlite") {
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
let url;
|
|
1133
|
+
try {
|
|
1134
|
+
url = new SafeURL(source.dsn);
|
|
1135
|
+
} catch {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
if (info) {
|
|
1139
|
+
if (source.host && info.host && source.host.toLowerCase() !== info.host.toLowerCase()) {
|
|
1140
|
+
conflict("host", source.host, info.host);
|
|
1141
|
+
}
|
|
1142
|
+
if (source.port !== void 0 && info.port !== void 0 && source.port !== info.port) {
|
|
1143
|
+
conflict("port", String(source.port), String(info.port));
|
|
1144
|
+
}
|
|
1145
|
+
if (source.database && info.database && source.database !== info.database) {
|
|
1146
|
+
conflict("database", source.database, info.database);
|
|
1147
|
+
}
|
|
1148
|
+
if (source.user && info.user && source.user !== info.user) {
|
|
1149
|
+
conflict("user", source.user, info.user);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (source.password && source.password !== url.password) {
|
|
1153
|
+
if (!url.password) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`Configuration file ${configPath}: source '${source.id}' has a 'password' field but the DSN has no password. The field is ignored at connection time \u2014 add the password to the DSN, or use individual connection parameters instead of a DSN.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
`Configuration file ${configPath}: source '${source.id}' has a 'password' field that conflicts with the password in the DSN. Set the password in only one place.`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
const dsnSslmode = getRawDSNQueryParam(source.dsn, "sslmode");
|
|
1163
|
+
if (source.sslmode && dsnSslmode !== null && dsnSslmode !== source.sslmode) {
|
|
1164
|
+
conflict("sslmode", source.sslmode, dsnSslmode);
|
|
1165
|
+
}
|
|
1166
|
+
const dsnSslrootcert = getRawDSNQueryParam(source.dsn, "sslrootcert");
|
|
1167
|
+
if (source.sslrootcert && dsnSslrootcert !== null && expandHomeDir(source.sslrootcert) !== expandHomeDir(dsnSslrootcert)) {
|
|
1168
|
+
conflict("sslrootcert", expandHomeDir(source.sslrootcert), expandHomeDir(dsnSslrootcert));
|
|
1169
|
+
}
|
|
1170
|
+
const dsnInstanceName = getRawDSNQueryParam(source.dsn, "instanceName");
|
|
1171
|
+
if (source.instanceName && dsnInstanceName !== null && dsnInstanceName !== source.instanceName) {
|
|
1172
|
+
conflict("instanceName", source.instanceName, dsnInstanceName);
|
|
1173
|
+
}
|
|
1174
|
+
const dsnAuthentication = getRawDSNQueryParam(source.dsn, "authentication");
|
|
1175
|
+
if (source.authentication && dsnAuthentication !== null && dsnAuthentication !== source.authentication) {
|
|
1176
|
+
conflict("authentication", source.authentication, dsnAuthentication);
|
|
1177
|
+
}
|
|
1178
|
+
const dsnDomain = getRawDSNQueryParam(source.dsn, "domain");
|
|
1179
|
+
if (source.domain && dsnDomain !== null && dsnDomain !== source.domain) {
|
|
1180
|
+
conflict("domain", source.domain, dsnDomain);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1097
1183
|
function validateSourceConfig(source, configPath) {
|
|
1098
1184
|
const hasConnectionParams = source.type && (source.type === "sqlite" ? source.database : source.host);
|
|
1099
1185
|
if (!source.dsn && !hasConnectionParams) {
|
|
@@ -1176,6 +1262,9 @@ function validateSourceConfig(source, configPath) {
|
|
|
1176
1262
|
);
|
|
1177
1263
|
}
|
|
1178
1264
|
}
|
|
1265
|
+
if (source.dsn) {
|
|
1266
|
+
validateDSNFieldConflicts(source, configPath);
|
|
1267
|
+
}
|
|
1179
1268
|
if (source.sslrootcert !== void 0) {
|
|
1180
1269
|
if (source.sslmode !== "verify-ca" && source.sslmode !== "verify-full") {
|
|
1181
1270
|
throw new Error(
|
|
@@ -1350,9 +1439,51 @@ function expandHomeDir(filePath) {
|
|
|
1350
1439
|
}
|
|
1351
1440
|
return filePath;
|
|
1352
1441
|
}
|
|
1442
|
+
function mergeSourceFieldsIntoDSN(dsn, source) {
|
|
1443
|
+
if (source.type === "sqlite") {
|
|
1444
|
+
return dsn;
|
|
1445
|
+
}
|
|
1446
|
+
try {
|
|
1447
|
+
new SafeURL(dsn);
|
|
1448
|
+
} catch {
|
|
1449
|
+
return dsn;
|
|
1450
|
+
}
|
|
1451
|
+
const hasParam = (key) => getRawDSNQueryParam(dsn, key) !== null;
|
|
1452
|
+
const additions = [];
|
|
1453
|
+
if (source.type === "sqlserver") {
|
|
1454
|
+
if (source.instanceName && !hasParam("instanceName")) {
|
|
1455
|
+
additions.push(`instanceName=${encodeURIComponent(source.instanceName)}`);
|
|
1456
|
+
}
|
|
1457
|
+
if (source.authentication && !hasParam("authentication")) {
|
|
1458
|
+
additions.push(`authentication=${encodeURIComponent(source.authentication)}`);
|
|
1459
|
+
}
|
|
1460
|
+
if (source.domain && !hasParam("domain")) {
|
|
1461
|
+
additions.push(`domain=${encodeURIComponent(source.domain)}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
if (source.sslmode && !hasParam("sslmode")) {
|
|
1465
|
+
additions.push(`sslmode=${source.sslmode}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (source.sslrootcert && source.type === "postgres" && (source.sslmode === "verify-ca" || source.sslmode === "verify-full") && !hasParam("sslrootcert")) {
|
|
1468
|
+
const expandedCertPath = expandHomeDir(source.sslrootcert);
|
|
1469
|
+
additions.push(`sslrootcert=${encodeURIComponent(expandedCertPath)}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (additions.length === 0) {
|
|
1472
|
+
return dsn;
|
|
1473
|
+
}
|
|
1474
|
+
let separator;
|
|
1475
|
+
if (!dsn.includes("?")) {
|
|
1476
|
+
separator = "?";
|
|
1477
|
+
} else if (dsn.endsWith("?") || dsn.endsWith("&")) {
|
|
1478
|
+
separator = "";
|
|
1479
|
+
} else {
|
|
1480
|
+
separator = "&";
|
|
1481
|
+
}
|
|
1482
|
+
return `${dsn}${separator}${additions.join("&")}`;
|
|
1483
|
+
}
|
|
1353
1484
|
function buildDSNFromSource(source) {
|
|
1354
1485
|
if (source.dsn) {
|
|
1355
|
-
return source.dsn;
|
|
1486
|
+
return mergeSourceFieldsIntoDSN(source.dsn, source);
|
|
1356
1487
|
}
|
|
1357
1488
|
if (!source.type) {
|
|
1358
1489
|
throw new Error(
|
|
@@ -1434,6 +1565,53 @@ async function generateRdsAuthToken(params) {
|
|
|
1434
1565
|
return signer.getAuthToken();
|
|
1435
1566
|
}
|
|
1436
1567
|
|
|
1568
|
+
// src/utils/error-classifier.ts
|
|
1569
|
+
var TUNNEL_ERROR_MARKER = "__dbhubSSHTunnelError";
|
|
1570
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
1571
|
+
"ECONNREFUSED",
|
|
1572
|
+
"ETIMEDOUT",
|
|
1573
|
+
"ENOTFOUND",
|
|
1574
|
+
"EHOSTUNREACH",
|
|
1575
|
+
"ENETUNREACH",
|
|
1576
|
+
"ECONNRESET"
|
|
1577
|
+
]);
|
|
1578
|
+
var AUTH_CODES = {
|
|
1579
|
+
postgres: ["28P01", "28000"],
|
|
1580
|
+
mysql: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1581
|
+
mariadb: ["ER_ACCESS_DENIED_ERROR", 1045, 1698],
|
|
1582
|
+
sqlserver: ["ELOGIN"],
|
|
1583
|
+
sqlite: []
|
|
1584
|
+
// no network/auth layer
|
|
1585
|
+
};
|
|
1586
|
+
function unreachableMessage(sourceId) {
|
|
1587
|
+
return `Source '${sourceId}' is unreachable. Verify the database is running and reachable (host, port, network), then retry.`;
|
|
1588
|
+
}
|
|
1589
|
+
function authMessage(sourceId) {
|
|
1590
|
+
return `Authentication failed for source '${sourceId}'. Verify the credentials/access for this source are valid, then retry.`;
|
|
1591
|
+
}
|
|
1592
|
+
function tunnelMessage(sourceId) {
|
|
1593
|
+
return `SSH tunnel for source '${sourceId}' failed to establish. Verify SSH host/credentials and bastion reachability, then retry.`;
|
|
1594
|
+
}
|
|
1595
|
+
function classifyConnectionError(error, connectorType, sourceId) {
|
|
1596
|
+
if (!error || typeof error !== "object") {
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
const err = error;
|
|
1600
|
+
if (err[TUNNEL_ERROR_MARKER] === true) {
|
|
1601
|
+
return { code: "TUNNEL_FAILED", message: tunnelMessage(sourceId) };
|
|
1602
|
+
}
|
|
1603
|
+
const code = err.code;
|
|
1604
|
+
if (typeof code === "string" && NETWORK_CODES.has(code)) {
|
|
1605
|
+
return { code: "SOURCE_UNREACHABLE", message: unreachableMessage(sourceId) };
|
|
1606
|
+
}
|
|
1607
|
+
const authCodes = AUTH_CODES[connectorType];
|
|
1608
|
+
const errno = err.errno;
|
|
1609
|
+
if (typeof code === "string" && authCodes.includes(code) || typeof errno === "number" && authCodes.includes(errno)) {
|
|
1610
|
+
return { code: "AUTH_FAILED", message: authMessage(sourceId) };
|
|
1611
|
+
}
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1437
1615
|
// src/connectors/manager.ts
|
|
1438
1616
|
var managerInstance = null;
|
|
1439
1617
|
var AWS_IAM_TOKEN_REFRESH_MS = 14 * 60 * 1e3;
|
|
@@ -1574,10 +1752,18 @@ var ConnectorManager = class {
|
|
|
1574
1752
|
const targetHost = url.hostname;
|
|
1575
1753
|
const targetPort = parseInt(url.port) || this.getDefaultPort(dsn);
|
|
1576
1754
|
const tunnel = new SSHTunnel();
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1755
|
+
let tunnelInfo;
|
|
1756
|
+
try {
|
|
1757
|
+
tunnelInfo = await tunnel.establish(sshConfig, {
|
|
1758
|
+
targetHost,
|
|
1759
|
+
targetPort
|
|
1760
|
+
});
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
if (error && typeof error === "object") {
|
|
1763
|
+
error[TUNNEL_ERROR_MARKER] = true;
|
|
1764
|
+
}
|
|
1765
|
+
throw error;
|
|
1766
|
+
}
|
|
1581
1767
|
url.hostname = "127.0.0.1";
|
|
1582
1768
|
url.port = tunnelInfo.localPort.toString();
|
|
1583
1769
|
actualDSN = url.toString();
|
|
@@ -2190,6 +2376,7 @@ export {
|
|
|
2190
2376
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
2191
2377
|
loadTomlConfig,
|
|
2192
2378
|
resolveTomlConfigPath,
|
|
2379
|
+
classifyConnectionError,
|
|
2193
2380
|
ConnectorManager,
|
|
2194
2381
|
mapArgumentsToArray,
|
|
2195
2382
|
ToolRegistry,
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
4
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
5
|
ConnectorManager,
|
|
6
|
+
classifyConnectionError,
|
|
6
7
|
getToolRegistry,
|
|
7
8
|
initializeToolRegistry,
|
|
8
9
|
isDemoMode,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
resolveSourceConfigs,
|
|
14
15
|
resolveTomlConfigPath,
|
|
15
16
|
resolveTransport
|
|
16
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-DN45LGY4.js";
|
|
17
18
|
import {
|
|
18
19
|
loadConnectors
|
|
19
20
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -99,7 +100,9 @@ var allowedKeywords = {
|
|
|
99
100
|
mysql: ["select", "with", "explain", "show", "describe", "desc"],
|
|
100
101
|
mariadb: ["select", "with", "explain", "show", "describe", "desc"],
|
|
101
102
|
sqlite: ["select", "with", "explain", "pragma"],
|
|
102
|
-
|
|
103
|
+
// SQL Server has no native EXPLAIN statement; the connector translates a
|
|
104
|
+
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
|
|
105
|
+
sqlserver: ["select", "with", "explain"]
|
|
103
106
|
};
|
|
104
107
|
var mutatingKeywords = [
|
|
105
108
|
"insert",
|
|
@@ -246,6 +249,20 @@ function trackToolRequest(metadata, startTime, extra, success, error) {
|
|
|
246
249
|
error
|
|
247
250
|
});
|
|
248
251
|
}
|
|
252
|
+
function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
|
|
253
|
+
let connectorType;
|
|
254
|
+
try {
|
|
255
|
+
connectorType = ConnectorManager.getSourceConfig(rawSourceId)?.type;
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (!connectorType) return null;
|
|
260
|
+
const classified = classifyConnectionError(error, connectorType, displaySourceId);
|
|
261
|
+
if (!classified) return null;
|
|
262
|
+
return createToolErrorResponse(classified.message, classified.code, {
|
|
263
|
+
source_id: displaySourceId
|
|
264
|
+
});
|
|
265
|
+
}
|
|
249
266
|
|
|
250
267
|
// src/tools/execute-sql.ts
|
|
251
268
|
var executeSqlSchema = {
|
|
@@ -290,6 +307,8 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
290
307
|
} catch (error) {
|
|
291
308
|
success = false;
|
|
292
309
|
errorMessage = error.message;
|
|
310
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
311
|
+
if (classified) return classified;
|
|
293
312
|
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
294
313
|
} finally {
|
|
295
314
|
trackToolRequest(
|
|
@@ -311,16 +330,25 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
311
330
|
import { z as z2 } from "zod";
|
|
312
331
|
var searchDatabaseObjectsSchema = {
|
|
313
332
|
object_type: z2.enum(["schema", "table", "view", "column", "procedure", "function", "index"]).describe("Object type to search"),
|
|
314
|
-
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)
|
|
333
|
+
pattern: z2.string().optional().default("%").describe("LIKE pattern (% = any chars, _ = one char)"),
|
|
315
334
|
schema: z2.string().optional().describe("Filter to schema"),
|
|
316
335
|
table: z2.string().optional().describe("Filter to table (requires schema; column/index only)"),
|
|
317
336
|
detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
|
|
318
|
-
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results
|
|
337
|
+
limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
|
|
319
338
|
};
|
|
320
339
|
function likePatternToRegex(pattern) {
|
|
321
340
|
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
322
341
|
return new RegExp(`^${escaped}$`, "i");
|
|
323
342
|
}
|
|
343
|
+
async function resolveDefaultSchemas(connector) {
|
|
344
|
+
if (connector.getDefaultSchema) {
|
|
345
|
+
const defaultSchema = await connector.getDefaultSchema();
|
|
346
|
+
if (defaultSchema) {
|
|
347
|
+
return [defaultSchema];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return connector.getSchemas();
|
|
351
|
+
}
|
|
324
352
|
async function getTableRowCount(connector, tableName, schemaName) {
|
|
325
353
|
try {
|
|
326
354
|
if (connector.getTableRowCount) {
|
|
@@ -348,7 +376,7 @@ async function getTableComment(connector, tableName, schemaName) {
|
|
|
348
376
|
}
|
|
349
377
|
}
|
|
350
378
|
async function searchSchemas(connector, pattern, detailLevel, limit) {
|
|
351
|
-
const schemas = await connector
|
|
379
|
+
const schemas = await resolveDefaultSchemas(connector);
|
|
352
380
|
const regex = likePatternToRegex(pattern);
|
|
353
381
|
const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
|
|
354
382
|
if (detailLevel === "names") {
|
|
@@ -379,7 +407,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
|
|
|
379
407
|
if (schemaFilter) {
|
|
380
408
|
schemasToSearch = [schemaFilter];
|
|
381
409
|
} else {
|
|
382
|
-
schemasToSearch = await connector
|
|
410
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
383
411
|
}
|
|
384
412
|
for (const schemaName of schemasToSearch) {
|
|
385
413
|
if (results.length >= limit) break;
|
|
@@ -532,7 +560,7 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
532
560
|
if (schemaFilter) {
|
|
533
561
|
schemasToSearch = [schemaFilter];
|
|
534
562
|
} else {
|
|
535
|
-
schemasToSearch = await connector
|
|
563
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
536
564
|
}
|
|
537
565
|
for (const schemaName of schemasToSearch) {
|
|
538
566
|
if (results.length >= limit) break;
|
|
@@ -589,7 +617,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
|
|
|
589
617
|
if (schemaFilter) {
|
|
590
618
|
schemasToSearch = [schemaFilter];
|
|
591
619
|
} else {
|
|
592
|
-
schemasToSearch = await connector
|
|
620
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
593
621
|
}
|
|
594
622
|
for (const schemaName of schemasToSearch) {
|
|
595
623
|
if (results.length >= limit) break;
|
|
@@ -637,7 +665,7 @@ async function searchIndexes(connector, pattern, schemaFilter, tableFilter, deta
|
|
|
637
665
|
if (schemaFilter) {
|
|
638
666
|
schemasToSearch = [schemaFilter];
|
|
639
667
|
} else {
|
|
640
|
-
schemasToSearch = await connector
|
|
668
|
+
schemasToSearch = await resolveDefaultSchemas(connector);
|
|
641
669
|
}
|
|
642
670
|
for (const schemaName of schemasToSearch) {
|
|
643
671
|
if (results.length >= limit) break;
|
|
@@ -760,6 +788,8 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
760
788
|
} catch (error) {
|
|
761
789
|
success = false;
|
|
762
790
|
errorMessage = error.message;
|
|
791
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
792
|
+
if (classified) return classified;
|
|
763
793
|
return createToolErrorResponse(
|
|
764
794
|
`Error searching database objects: ${errorMessage}`,
|
|
765
795
|
"SEARCH_ERROR"
|
|
@@ -864,7 +894,7 @@ function getSearchObjectsMetadata(sourceId) {
|
|
|
864
894
|
const toolName = isSingleSource ? "search_objects" : `search_objects_${normalizeSourceId(sourceId)}`;
|
|
865
895
|
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
866
896
|
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
867
|
-
const description = isSingleSource ? `${userDescPrefix}Search and list database objects
|
|
897
|
+
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
868
898
|
return {
|
|
869
899
|
name: toolName,
|
|
870
900
|
description,
|
|
@@ -905,7 +935,7 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
905
935
|
name: "object_type",
|
|
906
936
|
type: "string",
|
|
907
937
|
required: true,
|
|
908
|
-
description: "Object type to search"
|
|
938
|
+
description: "Object type to search: schema, table, view, column, procedure, function, index"
|
|
909
939
|
},
|
|
910
940
|
{
|
|
911
941
|
name: "pattern",
|
|
@@ -1053,6 +1083,8 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1053
1083
|
} catch (error) {
|
|
1054
1084
|
success = false;
|
|
1055
1085
|
errorMessage = error.message;
|
|
1086
|
+
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1087
|
+
if (classified) return classified;
|
|
1056
1088
|
if (error instanceof z4.ZodError) {
|
|
1057
1089
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1058
1090
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
@@ -1546,7 +1578,7 @@ See documentation for more details on configuring database connections.
|
|
|
1546
1578
|
const sources = sourceConfigsData.sources;
|
|
1547
1579
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1548
1580
|
await connectorManager.connectWithSources(sources);
|
|
1549
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1581
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-OF5GPCBK.js");
|
|
1550
1582
|
initializeToolRegistry2({
|
|
1551
1583
|
sources: sourceConfigsData.sources,
|
|
1552
1584
|
tools: sourceConfigsData.tools
|
|
@@ -1695,10 +1727,10 @@ See documentation for more details on configuring database connections.
|
|
|
1695
1727
|
// src/index.ts
|
|
1696
1728
|
var connectorModules = [
|
|
1697
1729
|
{ load: () => import("./postgres-EKVOBAH6.js"), name: "PostgreSQL", driver: "pg" },
|
|
1698
|
-
{ load: () => import("./sqlserver-
|
|
1730
|
+
{ load: () => import("./sqlserver-5TFVBCMA.js"), name: "SQL Server", driver: "mssql" },
|
|
1699
1731
|
{ load: () => import("./sqlite-2GBIF3MQ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1700
|
-
{ load: () => import("./mysql-
|
|
1701
|
-
{ load: () => import("./mariadb-
|
|
1732
|
+
{ load: () => import("./mysql-WG6UFALX.js"), name: "MySQL", driver: "mysql2" },
|
|
1733
|
+
{ load: () => import("./mariadb-ERB4D3RP.js"), name: "MariaDB", driver: "mariadb" }
|
|
1702
1734
|
];
|
|
1703
1735
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1704
1736
|
console.error("Fatal error:", error);
|
|
@@ -123,8 +123,9 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
123
123
|
}
|
|
124
124
|
try {
|
|
125
125
|
const rows = await this.pool.query(`
|
|
126
|
-
SELECT SCHEMA_NAME
|
|
126
|
+
SELECT SCHEMA_NAME
|
|
127
127
|
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
128
|
+
WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
|
128
129
|
ORDER BY SCHEMA_NAME
|
|
129
130
|
`);
|
|
130
131
|
return rows.map((row) => row.SCHEMA_NAME);
|
|
@@ -447,6 +448,18 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
447
448
|
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
448
449
|
return rows[0].DB;
|
|
449
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Default search scope = the database named in the DSN. DATABASE() returns
|
|
453
|
+
* null when the connection was opened without a database, in which case
|
|
454
|
+
* callers fall back to the full server-wide schema list.
|
|
455
|
+
*/
|
|
456
|
+
async getDefaultSchema() {
|
|
457
|
+
if (!this.pool) {
|
|
458
|
+
throw new Error("Not connected to database");
|
|
459
|
+
}
|
|
460
|
+
const rows = await this.pool.query("SELECT DATABASE() AS DB");
|
|
461
|
+
return rows[0]?.DB ?? null;
|
|
462
|
+
}
|
|
450
463
|
async executeSQL(sql, options, parameters) {
|
|
451
464
|
if (!this.pool) {
|
|
452
465
|
throw new Error("Not connected to database");
|
|
@@ -129,8 +129,9 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
129
129
|
}
|
|
130
130
|
try {
|
|
131
131
|
const [rows] = await this.pool.query(`
|
|
132
|
-
SELECT SCHEMA_NAME
|
|
132
|
+
SELECT SCHEMA_NAME
|
|
133
133
|
FROM INFORMATION_SCHEMA.SCHEMATA
|
|
134
|
+
WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
|
134
135
|
ORDER BY SCHEMA_NAME
|
|
135
136
|
`);
|
|
136
137
|
return rows.map((row) => row.SCHEMA_NAME);
|
|
@@ -453,6 +454,18 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
453
454
|
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
454
455
|
return rows[0].DB;
|
|
455
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Default search scope = the database named in the DSN. DATABASE() returns
|
|
459
|
+
* null when the connection was opened without a database, in which case
|
|
460
|
+
* callers fall back to the full server-wide schema list.
|
|
461
|
+
*/
|
|
462
|
+
async getDefaultSchema() {
|
|
463
|
+
if (!this.pool) {
|
|
464
|
+
throw new Error("Not connected to database");
|
|
465
|
+
}
|
|
466
|
+
const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
|
|
467
|
+
return rows[0]?.DB ?? null;
|
|
468
|
+
}
|
|
456
469
|
async executeSQL(sql, options, parameters) {
|
|
457
470
|
if (!this.pool) {
|
|
458
471
|
throw new Error("Not connected to database");
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
ConnectorRegistry,
|
|
9
9
|
SafeURL,
|
|
10
|
-
obfuscateDSNPassword
|
|
10
|
+
obfuscateDSNPassword,
|
|
11
|
+
stripCommentsAndStrings
|
|
11
12
|
} from "./chunk-C7WEAPX4.js";
|
|
12
13
|
|
|
13
14
|
// src/connectors/sqlserver/index.ts
|
|
@@ -135,7 +136,7 @@ Expected: ${expectedFormat}`
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
};
|
|
138
|
-
var
|
|
139
|
+
var _SQLServerConnector = class _SQLServerConnector {
|
|
139
140
|
constructor() {
|
|
140
141
|
this.id = "sqlserver";
|
|
141
142
|
this.name = "SQL Server";
|
|
@@ -457,6 +458,12 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
457
458
|
if (!this.connection) {
|
|
458
459
|
throw new Error("Not connected to SQL Server database");
|
|
459
460
|
}
|
|
461
|
+
const afterNoise = sqlQuery.slice(
|
|
462
|
+
sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
|
|
463
|
+
);
|
|
464
|
+
if (/^explain\b/i.test(afterNoise)) {
|
|
465
|
+
return this.explainQuery(afterNoise.slice("explain".length).trim());
|
|
466
|
+
}
|
|
460
467
|
try {
|
|
461
468
|
let processedSQL = sqlQuery;
|
|
462
469
|
if (options.maxRows) {
|
|
@@ -518,7 +525,60 @@ var SQLServerConnector = class _SQLServerConnector {
|
|
|
518
525
|
throw new Error(`Failed to execute query: ${error.message}`);
|
|
519
526
|
}
|
|
520
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Return the estimated execution plan for a query using SHOWPLAN_XML.
|
|
530
|
+
*
|
|
531
|
+
* SHOWPLAN_XML compiles the statement and returns its plan without executing
|
|
532
|
+
* it, but it has two constraints: `SET SHOWPLAN_XML ON` must be the only
|
|
533
|
+
* statement in its batch, and the setting is session scoped. The shared pool
|
|
534
|
+
* hands out a fresh connection per request() and an open transaction
|
|
535
|
+
* suppresses SHOWPLAN, so neither can carry the setting to a follow-up query.
|
|
536
|
+
*
|
|
537
|
+
* We therefore run the SET / query pair on a short-lived, single-connection
|
|
538
|
+
* pool built from the same config. The dedicated session keeps SHOWPLAN state
|
|
539
|
+
* off the shared pool, so a concurrent query can never land on a connection
|
|
540
|
+
* with SHOWPLAN enabled (which would return a plan instead of its results).
|
|
541
|
+
*/
|
|
542
|
+
async explainQuery(innerQuery) {
|
|
543
|
+
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
544
|
+
if (!cleaned) {
|
|
545
|
+
throw new Error("EXPLAIN requires a statement to analyze");
|
|
546
|
+
}
|
|
547
|
+
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
548
|
+
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
549
|
+
}
|
|
550
|
+
if (!this.config) {
|
|
551
|
+
throw new Error("Not connected to SQL Server database");
|
|
552
|
+
}
|
|
553
|
+
const explainPool = new sql.ConnectionPool({
|
|
554
|
+
...this.config,
|
|
555
|
+
pool: { ...this.config.pool, max: 1, min: 1 }
|
|
556
|
+
});
|
|
557
|
+
try {
|
|
558
|
+
await explainPool.connect();
|
|
559
|
+
await explainPool.request().batch("SET SHOWPLAN_XML ON");
|
|
560
|
+
const planResult = await explainPool.request().batch(innerQuery);
|
|
561
|
+
const planRow = planResult.recordset?.[0];
|
|
562
|
+
const planXml = planRow ? Object.values(planRow)[0] : null;
|
|
563
|
+
return {
|
|
564
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
565
|
+
rowCount: planXml != null ? 1 : 0
|
|
566
|
+
};
|
|
567
|
+
} catch (error) {
|
|
568
|
+
throw new Error(`Failed to explain query: ${error.message}`);
|
|
569
|
+
} finally {
|
|
570
|
+
await explainPool.close();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
521
573
|
};
|
|
574
|
+
/**
|
|
575
|
+
* Leading whitespace and SQL comments to skip before looking for a keyword.
|
|
576
|
+
* The read-only validator strips comments before checking the first keyword,
|
|
577
|
+
* so the connector must skip them too; otherwise an EXPLAIN preceded by a
|
|
578
|
+
* comment passes validation but reaches the server untranslated.
|
|
579
|
+
*/
|
|
580
|
+
_SQLServerConnector.LEADING_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
|
|
581
|
+
var SQLServerConnector = _SQLServerConnector;
|
|
522
582
|
var sqlServerConnector = new SQLServerConnector();
|
|
523
583
|
ConnectorRegistry.register(sqlServerConnector);
|
|
524
584
|
export {
|
package/package.json
CHANGED