@bytebase/dbhub 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/dist/{chunk-GSSLHJB6.js → chunk-2DFLBX4D.js} +38 -4
- package/dist/chunk-6QETXS44.js +254 -0
- package/dist/{chunk-NIILHXTG.js → chunk-A4TSDCPY.js} +26 -37
- package/dist/{chunk-EPOKWSFC.js → chunk-IQRBLAB4.js} +1 -1
- package/dist/{chunk-JEZZN2YZ.js → chunk-MF6LSVWK.js} +19 -1
- package/dist/index.js +77 -26
- package/dist/{mariadb-DUDFBY5W.js → mariadb-DQ6P57IX.js} +17 -12
- package/dist/{mysql-265M3KBD.js → mysql-GXDUNLJ6.js} +66 -14
- package/dist/{postgres-5ZOFYV6X.js → postgres-2FHS64EO.js} +33 -15
- package/dist/public/assets/{index-DBYlgGks.js → index-DOZ5HOZ9.js} +21 -20
- package/dist/public/index.html +1 -1
- package/dist/{registry-JYRRNC3E.js → registry-D2QVU6AG.js} +2 -2
- package/dist/{sqlite-5M7SDAQZ.js → sqlite-ASYMU5BI.js} +28 -19
- package/dist/{sqlserver-PSAZRH7G.js → sqlserver-WNVSBFJ3.js} +90 -14
- package/package.json +5 -2
- package/dist/chunk-S2V6RZ3C.js +0 -138
package/dist/index.js
CHANGED
|
@@ -12,19 +12,20 @@ import {
|
|
|
12
12
|
loadTomlConfig,
|
|
13
13
|
mapArgumentsToArray,
|
|
14
14
|
resolveAllowedHosts,
|
|
15
|
+
resolveAuthTokens,
|
|
15
16
|
resolveHost,
|
|
16
17
|
resolvePort,
|
|
17
18
|
resolveSourceConfigs,
|
|
18
19
|
resolveTomlConfigPath,
|
|
19
20
|
resolveTransport
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-2DFLBX4D.js";
|
|
21
22
|
import {
|
|
22
23
|
allowedKeywords,
|
|
23
24
|
getFirstKeyword,
|
|
24
25
|
hasEscapeHatchFunction,
|
|
25
26
|
isReadOnlySQL,
|
|
26
27
|
sqlServerDynamicSqlPattern
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IQRBLAB4.js";
|
|
28
29
|
import {
|
|
29
30
|
loadConnectors
|
|
30
31
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -38,7 +39,7 @@ import {
|
|
|
38
39
|
parseConnectionInfoFromDSN,
|
|
39
40
|
splitSQLStatements,
|
|
40
41
|
stripCommentsAndStrings
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-MF6LSVWK.js";
|
|
42
43
|
|
|
43
44
|
// src/server.ts
|
|
44
45
|
import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
|
|
@@ -219,6 +220,17 @@ function normalizeSourceId(id) {
|
|
|
219
220
|
function getEffectiveSourceId(sourceId) {
|
|
220
221
|
return sourceId || "default";
|
|
221
222
|
}
|
|
223
|
+
function toStatementsPayload(resultSets) {
|
|
224
|
+
return resultSets.map((set) => ({
|
|
225
|
+
sql: set.sql,
|
|
226
|
+
rows: set.rows,
|
|
227
|
+
count: set.rowCount,
|
|
228
|
+
// Only present when a max_rows cap actually cut off rows, so consumers
|
|
229
|
+
// can tell a capped result from a table with exactly max_rows rows
|
|
230
|
+
// (and complete results don't pay the extra tokens).
|
|
231
|
+
...set.truncated ? { truncated: true } : {}
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
222
234
|
function resolveTrackedToolName(sourceId, baseName) {
|
|
223
235
|
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
224
236
|
if (isSingleSource) {
|
|
@@ -288,8 +300,7 @@ function createExecuteSqlToolHandler(sourceId) {
|
|
|
288
300
|
};
|
|
289
301
|
result = await connector.executeSQL(sql, executeOptions);
|
|
290
302
|
const responseData = {
|
|
291
|
-
|
|
292
|
-
count: result.rowCount,
|
|
303
|
+
statements: toStatementsPayload(result.resultSets),
|
|
293
304
|
source_id: effectiveSourceId,
|
|
294
305
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
295
306
|
};
|
|
@@ -348,8 +359,9 @@ async function getTableRowCount(connector, tableName, schemaName) {
|
|
|
348
359
|
const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
|
|
349
360
|
const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
|
|
350
361
|
const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
|
|
351
|
-
|
|
352
|
-
|
|
362
|
+
const rows = result.resultSets[0]?.rows;
|
|
363
|
+
if (rows && rows.length > 0) {
|
|
364
|
+
return Number(rows[0].count || rows[0].COUNT || 0);
|
|
353
365
|
}
|
|
354
366
|
} catch (error) {
|
|
355
367
|
return null;
|
|
@@ -852,8 +864,8 @@ function createExplainSqlToolHandler(sourceId) {
|
|
|
852
864
|
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
853
865
|
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
854
866
|
const responseData = {
|
|
855
|
-
rows: result.rows,
|
|
856
|
-
count: result.rowCount,
|
|
867
|
+
rows: result.resultSets[0].rows,
|
|
868
|
+
count: result.resultSets[0].rowCount,
|
|
857
869
|
source_id: effectiveSourceId,
|
|
858
870
|
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
859
871
|
};
|
|
@@ -1000,7 +1012,7 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
1000
1012
|
maxRows: toolConfig?.max_rows
|
|
1001
1013
|
};
|
|
1002
1014
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
1003
|
-
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
1015
|
+
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows; capped results carry "truncated": true)` : "";
|
|
1004
1016
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
1005
1017
|
const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
|
|
1006
1018
|
const annotations = {
|
|
@@ -1036,7 +1048,7 @@ function getExplainSqlMetadata(sourceId) {
|
|
|
1036
1048
|
"explain_sql",
|
|
1037
1049
|
"Explain Query Plan"
|
|
1038
1050
|
);
|
|
1039
|
-
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it
|
|
1051
|
+
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it (always read-only).` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it (always read-only).`;
|
|
1040
1052
|
const annotations = {
|
|
1041
1053
|
title,
|
|
1042
1054
|
readOnlyHint: true,
|
|
@@ -1057,7 +1069,7 @@ function getHealthCheckMetadata(sourceId) {
|
|
|
1057
1069
|
"health_check",
|
|
1058
1070
|
"Health Check"
|
|
1059
1071
|
);
|
|
1060
|
-
const description = isSingleSource ? `${userDescPrefix}Report
|
|
1072
|
+
const description = isSingleSource ? `${userDescPrefix}Report connection pool and buffer cache health metrics for the ${dbType} database (read-only).` : `${userDescPrefix}Report connection pool and buffer cache health metrics for the '${sourceId}' ${dbType} database (read-only).`;
|
|
1061
1073
|
const annotations = {
|
|
1062
1074
|
title,
|
|
1063
1075
|
readOnlyHint: true,
|
|
@@ -1107,13 +1119,13 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
1107
1119
|
name: "object_type",
|
|
1108
1120
|
type: "string",
|
|
1109
1121
|
required: true,
|
|
1110
|
-
description: "Object type to search
|
|
1122
|
+
description: "Object type to search"
|
|
1111
1123
|
},
|
|
1112
1124
|
{
|
|
1113
1125
|
name: "pattern",
|
|
1114
1126
|
type: "string",
|
|
1115
1127
|
required: false,
|
|
1116
|
-
description: "LIKE pattern (% = any chars, _ = one char)
|
|
1128
|
+
description: "LIKE pattern (% = any chars, _ = one char)"
|
|
1117
1129
|
},
|
|
1118
1130
|
{
|
|
1119
1131
|
name: "schema",
|
|
@@ -1137,7 +1149,7 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
1137
1149
|
name: "limit",
|
|
1138
1150
|
type: "integer",
|
|
1139
1151
|
required: false,
|
|
1140
|
-
description: "Max results
|
|
1152
|
+
description: "Max results"
|
|
1141
1153
|
}
|
|
1142
1154
|
],
|
|
1143
1155
|
readonly: true
|
|
@@ -1267,8 +1279,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1267
1279
|
paramValues
|
|
1268
1280
|
);
|
|
1269
1281
|
const responseData = {
|
|
1270
|
-
|
|
1271
|
-
count: result.rowCount,
|
|
1282
|
+
statements: toStatementsPayload(result.resultSets),
|
|
1272
1283
|
source_id: toolConfig.source
|
|
1273
1284
|
};
|
|
1274
1285
|
return createToolSuccessResponse(responseData);
|
|
@@ -1822,6 +1833,32 @@ function validateOrigin(originHeader, hostHeader, allowedHosts) {
|
|
|
1822
1833
|
return { ok: true };
|
|
1823
1834
|
}
|
|
1824
1835
|
|
|
1836
|
+
// src/utils/auth-token.ts
|
|
1837
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1838
|
+
var BEARER_PREFIX = "Bearer ";
|
|
1839
|
+
function constantTimeEqual(a, b) {
|
|
1840
|
+
const bufA = Buffer.from(a);
|
|
1841
|
+
const bufB = Buffer.from(b);
|
|
1842
|
+
if (bufA.length !== bufB.length) return false;
|
|
1843
|
+
return timingSafeEqual(bufA, bufB);
|
|
1844
|
+
}
|
|
1845
|
+
function validateAuthToken(authorizationHeader, tokens) {
|
|
1846
|
+
if (tokens.length === 0) return { ok: true };
|
|
1847
|
+
if (!authorizationHeader || !authorizationHeader.startsWith(BEARER_PREFIX)) {
|
|
1848
|
+
return {
|
|
1849
|
+
ok: false,
|
|
1850
|
+
status: 401,
|
|
1851
|
+
message: "Missing or malformed Authorization header. Expected: Bearer <token>"
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1854
|
+
const presented = authorizationHeader.slice(BEARER_PREFIX.length);
|
|
1855
|
+
const matches = tokens.some((token) => constantTimeEqual(presented, token));
|
|
1856
|
+
if (!matches) {
|
|
1857
|
+
return { ok: false, status: 401, message: "Invalid bearer token" };
|
|
1858
|
+
}
|
|
1859
|
+
return { ok: true };
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1825
1862
|
// src/server.ts
|
|
1826
1863
|
var __filename = fileURLToPath(import.meta.url);
|
|
1827
1864
|
var __dirname = path.dirname(__filename);
|
|
@@ -1876,7 +1913,7 @@ See documentation for more details on configuring database connections.
|
|
|
1876
1913
|
const sources = sourceConfigsData.sources;
|
|
1877
1914
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1878
1915
|
await connectorManager.connectWithSources(sources);
|
|
1879
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1916
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-D2QVU6AG.js");
|
|
1880
1917
|
initializeToolRegistry2({
|
|
1881
1918
|
sources: sourceConfigsData.sources,
|
|
1882
1919
|
tools: sourceConfigsData.tools
|
|
@@ -1906,6 +1943,7 @@ See documentation for more details on configuring database connections.
|
|
|
1906
1943
|
const port = transportData.type === "http" ? resolvePort().port : null;
|
|
1907
1944
|
const host = transportData.type === "http" ? resolveHost().host : null;
|
|
1908
1945
|
const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
|
|
1946
|
+
const { tokens: authTokens, source: authTokenSource } = resolveAuthTokens();
|
|
1909
1947
|
const activeModes = [];
|
|
1910
1948
|
const modeDescriptions = [];
|
|
1911
1949
|
const isDemo = isDemoMode();
|
|
@@ -1940,18 +1978,26 @@ See documentation for more details on configuring database connections.
|
|
|
1940
1978
|
}
|
|
1941
1979
|
res.header("Access-Control-Allow-Origin", origin || "http://localhost");
|
|
1942
1980
|
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1943
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1981
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
|
|
1944
1982
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
1945
1983
|
if (req.method === "OPTIONS") {
|
|
1946
1984
|
return res.sendStatus(200);
|
|
1947
1985
|
}
|
|
1948
1986
|
next();
|
|
1949
1987
|
});
|
|
1950
|
-
const frontendPath = path.join(__dirname, "public");
|
|
1951
|
-
app.use(express.static(frontendPath));
|
|
1952
1988
|
app.get("/healthz", (req, res) => {
|
|
1953
1989
|
res.status(200).send("OK");
|
|
1954
1990
|
});
|
|
1991
|
+
app.use((req, res, next) => {
|
|
1992
|
+
const result = validateAuthToken(req.headers.authorization, authTokens);
|
|
1993
|
+
if (!result.ok) {
|
|
1994
|
+
res.header("WWW-Authenticate", "Bearer");
|
|
1995
|
+
return res.status(result.status).json({ error: "Unauthorized", message: result.message });
|
|
1996
|
+
}
|
|
1997
|
+
next();
|
|
1998
|
+
});
|
|
1999
|
+
const frontendPath = path.join(__dirname, "public");
|
|
2000
|
+
app.use(express.static(frontendPath));
|
|
1955
2001
|
app.get("/api/sources", listSources);
|
|
1956
2002
|
app.get("/api/sources/:sourceId", getSource);
|
|
1957
2003
|
app.get("/api/requests", listRequests);
|
|
@@ -1983,6 +2029,11 @@ See documentation for more details on configuring database connections.
|
|
|
1983
2029
|
} else {
|
|
1984
2030
|
console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
|
|
1985
2031
|
}
|
|
2032
|
+
if (authTokens.length > 0) {
|
|
2033
|
+
console.error(`Auth: bearer token required (${authTokens.length} token(s) configured via ${authTokenSource})`);
|
|
2034
|
+
} else {
|
|
2035
|
+
console.error("Auth: disabled (set --auth-token or DBHUB_AUTH_TOKEN to require a bearer token)");
|
|
2036
|
+
}
|
|
1986
2037
|
if (process.env.NODE_ENV === "development") {
|
|
1987
2038
|
console.error("Development mode detected!");
|
|
1988
2039
|
console.error(" Workbench dev server (with HMR): http://localhost:5173");
|
|
@@ -2017,11 +2068,11 @@ See documentation for more details on configuring database connections.
|
|
|
2017
2068
|
|
|
2018
2069
|
// src/index.ts
|
|
2019
2070
|
var connectorModules = [
|
|
2020
|
-
{ load: () => import("./postgres-
|
|
2021
|
-
{ load: () => import("./sqlserver-
|
|
2022
|
-
{ load: () => import("./sqlite-
|
|
2023
|
-
{ load: () => import("./mysql-
|
|
2024
|
-
{ load: () => import("./mariadb-
|
|
2071
|
+
{ load: () => import("./postgres-2FHS64EO.js"), name: "PostgreSQL", driver: "pg" },
|
|
2072
|
+
{ load: () => import("./sqlserver-WNVSBFJ3.js"), name: "SQL Server", driver: "mssql" },
|
|
2073
|
+
{ load: () => import("./sqlite-ASYMU5BI.js"), name: "SQLite", driver: "node:sqlite" },
|
|
2074
|
+
{ load: () => import("./mysql-GXDUNLJ6.js"), name: "MySQL", driver: "mysql2" },
|
|
2075
|
+
{ load: () => import("./mariadb-DQ6P57IX.js"), name: "MariaDB", driver: "mariadb" }
|
|
2025
2076
|
];
|
|
2026
2077
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
2027
2078
|
console.error("Fatal error:", error);
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingDatabaseError,
|
|
3
|
-
extractAffectedRows,
|
|
4
3
|
getMySQLFamilyHealthCheck,
|
|
5
4
|
isTiDBVersion,
|
|
6
|
-
|
|
5
|
+
parseQueryResultSets,
|
|
7
6
|
requireDatabaseInDSN,
|
|
8
7
|
withReadOnlyTransaction
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-A4TSDCPY.js";
|
|
10
9
|
import "./chunk-FU2ZJE4E.js";
|
|
11
10
|
import {
|
|
12
11
|
quoteIdentifier
|
|
@@ -14,13 +13,13 @@ import {
|
|
|
14
13
|
import {
|
|
15
14
|
SQLRowLimiter,
|
|
16
15
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-6QETXS44.js";
|
|
18
17
|
import {
|
|
19
18
|
ConnectorRegistry,
|
|
20
19
|
SafeURL,
|
|
21
20
|
obfuscateDSNPassword,
|
|
22
21
|
splitSQLStatements
|
|
23
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-MF6LSVWK.js";
|
|
24
23
|
|
|
25
24
|
// src/connectors/mariadb/index.ts
|
|
26
25
|
import * as mariadb from "mariadb";
|
|
@@ -505,13 +504,15 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
505
504
|
options.readonly,
|
|
506
505
|
this.supportsReadOnlyTransaction,
|
|
507
506
|
async () => {
|
|
507
|
+
const statements = splitSQLStatements(sql, "mariadb");
|
|
508
508
|
let processedSQL = sql;
|
|
509
|
+
let probes = [];
|
|
509
510
|
if (options.maxRows) {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
511
|
+
const rewrites = statements.map(
|
|
512
|
+
(statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
|
|
513
513
|
);
|
|
514
|
-
|
|
514
|
+
probes = rewrites.map((rewrite) => rewrite.probeApplied);
|
|
515
|
+
processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
|
|
515
516
|
if (sql.trim().endsWith(";")) {
|
|
516
517
|
processedSQL += ";";
|
|
517
518
|
}
|
|
@@ -522,9 +523,13 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
522
523
|
} else {
|
|
523
524
|
results = await conn.query(processedSQL);
|
|
524
525
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
526
|
+
const resultSets = parseQueryResultSets(results, statements);
|
|
527
|
+
if (resultSets.length === probes.length) {
|
|
528
|
+
resultSets.forEach(
|
|
529
|
+
(set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return { resultSets };
|
|
528
533
|
}
|
|
529
534
|
);
|
|
530
535
|
} finally {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingDatabaseError,
|
|
3
|
-
extractAffectedRows,
|
|
4
3
|
getMySQLFamilyHealthCheck,
|
|
4
|
+
isClientSideTimeout,
|
|
5
5
|
isTiDBVersion,
|
|
6
|
-
|
|
6
|
+
parseQueryResultSets,
|
|
7
7
|
requireDatabaseInDSN,
|
|
8
8
|
withReadOnlyTransaction
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-A4TSDCPY.js";
|
|
10
10
|
import "./chunk-FU2ZJE4E.js";
|
|
11
11
|
import {
|
|
12
12
|
quoteIdentifier
|
|
@@ -14,13 +14,13 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
SQLRowLimiter,
|
|
16
16
|
closeQuietly
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-6QETXS44.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
20
|
SafeURL,
|
|
21
21
|
obfuscateDSNPassword,
|
|
22
22
|
splitSQLStatements
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-MF6LSVWK.js";
|
|
24
24
|
|
|
25
25
|
// src/connectors/mysql/index.ts
|
|
26
26
|
import mysql from "mysql2/promise";
|
|
@@ -106,7 +106,7 @@ Expected: ${expectedFormat}`
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
};
|
|
109
|
-
var
|
|
109
|
+
var _MySQLConnector = class _MySQLConnector {
|
|
110
110
|
constructor() {
|
|
111
111
|
this.id = "mysql";
|
|
112
112
|
this.name = "MySQL";
|
|
@@ -507,19 +507,23 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
507
507
|
throw new Error("Not connected to database");
|
|
508
508
|
}
|
|
509
509
|
const conn = await this.pool.getConnection();
|
|
510
|
+
const threadId = conn.threadId;
|
|
511
|
+
let isConnectionPoisoned = false;
|
|
510
512
|
try {
|
|
511
513
|
return await withReadOnlyTransaction(
|
|
512
514
|
conn,
|
|
513
515
|
options.readonly,
|
|
514
516
|
this.supportsReadOnlyTransaction,
|
|
515
517
|
async () => {
|
|
518
|
+
const statements = splitSQLStatements(sql, "mysql");
|
|
516
519
|
let processedSQL = sql;
|
|
520
|
+
let probes = [];
|
|
517
521
|
if (options.maxRows) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
(statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
|
|
522
|
+
const rewrites = statements.map(
|
|
523
|
+
(statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
|
|
521
524
|
);
|
|
522
|
-
|
|
525
|
+
probes = rewrites.map((rewrite) => rewrite.probeApplied);
|
|
526
|
+
processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; ");
|
|
523
527
|
if (sql.trim().endsWith(";")) {
|
|
524
528
|
processedSQL += ";";
|
|
525
529
|
}
|
|
@@ -531,16 +535,64 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
531
535
|
results = await conn.query({ sql: processedSQL, timeout: this.queryTimeoutMs });
|
|
532
536
|
}
|
|
533
537
|
const [firstResult] = results;
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
538
|
+
const resultSets = parseQueryResultSets(firstResult, statements);
|
|
539
|
+
if (resultSets.length === probes.length) {
|
|
540
|
+
resultSets.forEach(
|
|
541
|
+
(set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index])
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return { resultSets };
|
|
537
545
|
}
|
|
538
546
|
);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (isClientSideTimeout(error)) {
|
|
549
|
+
isConnectionPoisoned = true;
|
|
550
|
+
await this.killQuery(threadId);
|
|
551
|
+
}
|
|
552
|
+
throw error;
|
|
553
|
+
} finally {
|
|
554
|
+
if (isConnectionPoisoned) {
|
|
555
|
+
conn.destroy();
|
|
556
|
+
} else {
|
|
557
|
+
conn.release();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Best-effort server-side kill for a query abandoned by mysql2's client-side
|
|
563
|
+
* timeout. Uses a separate connection: the original connection's command
|
|
564
|
+
* queue is stuck behind the abandoned statement (see isClientSideTimeout)
|
|
565
|
+
* and cannot itself be used to send KILL QUERY.
|
|
566
|
+
*
|
|
567
|
+
* Bounded by its own short timeout, independent of the user's (possibly
|
|
568
|
+
* long or unset) query_timeout — KILL QUERY is metadata-only and should
|
|
569
|
+
* return almost immediately on a healthy server, so cleanup must not stall
|
|
570
|
+
* indefinitely if it doesn't.
|
|
571
|
+
*/
|
|
572
|
+
async killQuery(threadId) {
|
|
573
|
+
if (!this.pool) return;
|
|
574
|
+
let killer;
|
|
575
|
+
let killerPoisoned = false;
|
|
576
|
+
try {
|
|
577
|
+
killer = await this.pool.getConnection();
|
|
578
|
+
await killer.query({ sql: `KILL QUERY ${threadId}`, timeout: _MySQLConnector.KILL_QUERY_TIMEOUT_MS });
|
|
579
|
+
} catch (error) {
|
|
580
|
+
killerPoisoned = isClientSideTimeout(error);
|
|
539
581
|
} finally {
|
|
540
|
-
|
|
582
|
+
if (killer) {
|
|
583
|
+
if (killerPoisoned) {
|
|
584
|
+
killer.destroy();
|
|
585
|
+
} else {
|
|
586
|
+
killer.release();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
541
589
|
}
|
|
542
590
|
}
|
|
543
591
|
};
|
|
592
|
+
// Bounds the KILL QUERY cleanup call issued after a client-side query
|
|
593
|
+
// timeout; see killQuery.
|
|
594
|
+
_MySQLConnector.KILL_QUERY_TIMEOUT_MS = 5e3;
|
|
595
|
+
var MySQLConnector = _MySQLConnector;
|
|
544
596
|
var mysqlConnector = new MySQLConnector();
|
|
545
597
|
ConnectorRegistry.register(mysqlConnector);
|
|
546
598
|
export {
|
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
SQLRowLimiter,
|
|
9
9
|
closeQuietly
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-6QETXS44.js";
|
|
11
11
|
import {
|
|
12
12
|
ConnectorRegistry,
|
|
13
13
|
SafeURL,
|
|
14
14
|
obfuscateDSNPassword,
|
|
15
15
|
splitSQLStatements
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-MF6LSVWK.js";
|
|
17
17
|
|
|
18
18
|
// src/connectors/postgres/index.ts
|
|
19
19
|
import fs from "fs";
|
|
@@ -557,13 +557,22 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
557
557
|
try {
|
|
558
558
|
const statements = splitSQLStatements(sql, "postgres");
|
|
559
559
|
if (statements.length === 1) {
|
|
560
|
-
const processedStatement = SQLRowLimiter.
|
|
560
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
561
|
+
statements[0],
|
|
562
|
+
options.maxRows
|
|
563
|
+
);
|
|
561
564
|
if (options.readonly) {
|
|
562
565
|
await client.query("BEGIN READ ONLY");
|
|
563
566
|
try {
|
|
564
567
|
const result2 = parameters && parameters.length > 0 ? await client.query(processedStatement, parameters) : await client.query(processedStatement);
|
|
565
568
|
await client.query("COMMIT");
|
|
566
|
-
|
|
569
|
+
const resultSet2 = {
|
|
570
|
+
sql: statements[0],
|
|
571
|
+
rows: result2.rows,
|
|
572
|
+
rowCount: result2.rowCount ?? result2.rows.length
|
|
573
|
+
};
|
|
574
|
+
SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
|
|
575
|
+
return { resultSets: [resultSet2] };
|
|
567
576
|
} catch (error) {
|
|
568
577
|
try {
|
|
569
578
|
await client.query("ROLLBACK");
|
|
@@ -578,24 +587,33 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
578
587
|
} else {
|
|
579
588
|
result = await client.query(processedStatement);
|
|
580
589
|
}
|
|
581
|
-
|
|
590
|
+
const resultSet = {
|
|
591
|
+
sql: statements[0],
|
|
592
|
+
rows: result.rows,
|
|
593
|
+
rowCount: result.rowCount ?? result.rows.length
|
|
594
|
+
};
|
|
595
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
596
|
+
return { resultSets: [resultSet] };
|
|
582
597
|
} else {
|
|
583
598
|
if (parameters && parameters.length > 0) {
|
|
584
599
|
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
|
|
585
600
|
}
|
|
586
|
-
|
|
587
|
-
let totalRowCount = 0;
|
|
601
|
+
const resultSets = [];
|
|
588
602
|
await client.query(options.readonly ? "BEGIN READ ONLY" : "BEGIN");
|
|
589
603
|
try {
|
|
590
604
|
for (let statement of statements) {
|
|
591
|
-
const processedStatement = SQLRowLimiter.
|
|
605
|
+
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
|
|
606
|
+
statement,
|
|
607
|
+
options.maxRows
|
|
608
|
+
);
|
|
592
609
|
const result = await client.query(processedStatement);
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
610
|
+
const resultSet = {
|
|
611
|
+
sql: statement,
|
|
612
|
+
rows: result.rows ?? [],
|
|
613
|
+
rowCount: result.rowCount ?? result.rows?.length ?? 0
|
|
614
|
+
};
|
|
615
|
+
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
|
|
616
|
+
resultSets.push(resultSet);
|
|
599
617
|
}
|
|
600
618
|
await client.query("COMMIT");
|
|
601
619
|
} catch (error) {
|
|
@@ -605,7 +623,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
605
623
|
}
|
|
606
624
|
throw error;
|
|
607
625
|
}
|
|
608
|
-
return {
|
|
626
|
+
return { resultSets };
|
|
609
627
|
}
|
|
610
628
|
} finally {
|
|
611
629
|
client.release();
|