@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/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-GSSLHJB6.js";
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-EPOKWSFC.js";
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-JEZZN2YZ.js";
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
- rows: result.rows,
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
- if (result.rows && result.rows.length > 0) {
352
- return Number(result.rows[0].count || result.rows[0].COUNT || 0);
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. Always read-only and safe, independent of the source's read/write mode.` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.`;
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 operational health metrics (connection pool state, buffer cache hit ratio) for the ${dbType} database. Read-only, independent of the source's read/write mode.` : `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the '${sourceId}' ${dbType} database. Read-only, independent of the source's read/write mode.`;
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: schema, table, view, column, procedure, function, index"
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). Default: %"
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 (default: 100, max: 1000)"
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
- rows: result.rows,
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-JYRRNC3E.js");
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-5ZOFYV6X.js"), name: "PostgreSQL", driver: "pg" },
2021
- { load: () => import("./sqlserver-PSAZRH7G.js"), name: "SQL Server", driver: "mssql" },
2022
- { load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
2023
- { load: () => import("./mysql-265M3KBD.js"), name: "MySQL", driver: "mysql2" },
2024
- { load: () => import("./mariadb-DUDFBY5W.js"), name: "MariaDB", driver: "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
- parseQueryResults,
5
+ parseQueryResultSets,
7
6
  requireDatabaseInDSN,
8
7
  withReadOnlyTransaction
9
- } from "./chunk-NIILHXTG.js";
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-S2V6RZ3C.js";
16
+ } from "./chunk-6QETXS44.js";
18
17
  import {
19
18
  ConnectorRegistry,
20
19
  SafeURL,
21
20
  obfuscateDSNPassword,
22
21
  splitSQLStatements
23
- } from "./chunk-JEZZN2YZ.js";
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 statements = splitSQLStatements(sql, "mariadb");
511
- const processedStatements = statements.map(
512
- (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
511
+ const rewrites = statements.map(
512
+ (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
513
513
  );
514
- processedSQL = processedStatements.join("; ");
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 rows = parseQueryResults(results);
526
- const rowCount = extractAffectedRows(results);
527
- return { rows, rowCount };
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
- parseQueryResults,
6
+ parseQueryResultSets,
7
7
  requireDatabaseInDSN,
8
8
  withReadOnlyTransaction
9
- } from "./chunk-NIILHXTG.js";
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-S2V6RZ3C.js";
17
+ } from "./chunk-6QETXS44.js";
18
18
  import {
19
19
  ConnectorRegistry,
20
20
  SafeURL,
21
21
  obfuscateDSNPassword,
22
22
  splitSQLStatements
23
- } from "./chunk-JEZZN2YZ.js";
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 MySQLConnector = class _MySQLConnector {
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 statements = splitSQLStatements(sql, "mysql");
519
- const processedStatements = statements.map(
520
- (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
522
+ const rewrites = statements.map(
523
+ (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows)
521
524
  );
522
- processedSQL = processedStatements.join("; ");
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 rows = parseQueryResults(firstResult);
535
- const rowCount = extractAffectedRows(firstResult);
536
- return { rows, rowCount };
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
- conn.release();
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-S2V6RZ3C.js";
10
+ } from "./chunk-6QETXS44.js";
11
11
  import {
12
12
  ConnectorRegistry,
13
13
  SafeURL,
14
14
  obfuscateDSNPassword,
15
15
  splitSQLStatements
16
- } from "./chunk-JEZZN2YZ.js";
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.applyMaxRows(statements[0], options.maxRows);
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
- return { rows: result2.rows, rowCount: result2.rowCount ?? result2.rows.length };
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
- return { rows: result.rows, rowCount: result.rowCount ?? result.rows.length };
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
- let allRows = [];
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.applyMaxRows(statement, options.maxRows);
605
+ const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
606
+ statement,
607
+ options.maxRows
608
+ );
592
609
  const result = await client.query(processedStatement);
593
- if (result.rows && result.rows.length > 0) {
594
- allRows.push(...result.rows);
595
- }
596
- if (result.rowCount) {
597
- totalRowCount += result.rowCount;
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 { rows: allRows, rowCount: totalRowCount };
626
+ return { resultSets };
609
627
  }
610
628
  } finally {
611
629
  client.release();