@bytebase/dbhub 0.22.4 → 0.22.6

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 CHANGED
@@ -93,6 +93,8 @@ npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
93
93
  ```
94
94
 
95
95
  > The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
96
+ >
97
+ > The HTTP transport also has built-in DNS-rebinding protection: it only accepts requests whose `Host` is loopback, this machine's own hostname/IPs, or a name you allow via [`--allowed-hosts`](https://dbhub.ai/config/command-line#allowed-hosts). If a client behind a reverse proxy or custom DNS name gets a `403`, add that hostname with `--allowed-hosts`.
96
98
 
97
99
  See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
98
100
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stripCommentsAndStrings
3
- } from "./chunk-C7WEAPX4.js";
3
+ } from "./chunk-JEZZN2YZ.js";
4
4
 
5
5
  // src/utils/sql-row-limiter.ts
6
6
  var SQLRowLimiter = class {
@@ -267,6 +267,20 @@ function scanSingleLineComment(sql, i) {
267
267
  }
268
268
  return { type: TokenType.Comment, end: j };
269
269
  }
270
+ function scanSingleLineCommentMySQL(sql, i) {
271
+ if (sql[i] !== "-" || sql[i + 1] !== "-") {
272
+ return null;
273
+ }
274
+ const next = sql[i + 2];
275
+ if (next !== void 0 && next.charCodeAt(0) > 32 && next.charCodeAt(0) !== 127) {
276
+ return null;
277
+ }
278
+ let j = i;
279
+ while (j < sql.length && sql[j] !== "\n") {
280
+ j++;
281
+ }
282
+ return { type: TokenType.Comment, end: j };
283
+ }
270
284
  function scanMultiLineComment(sql, i) {
271
285
  if (sql[i] !== "/" || sql[i + 1] !== "*") {
272
286
  return null;
@@ -405,7 +419,7 @@ function scanTokenPostgres(sql, i) {
405
419
  return scanSingleLineComment(sql, i) ?? scanNestedMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanDollarQuotedBlock(sql, i) ?? plainToken(i);
406
420
  }
407
421
  function scanTokenMySQL(sql, i) {
408
- return scanSingleLineComment(sql, i) ?? scanMultiLineCommentMySQL(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
422
+ return scanSingleLineCommentMySQL(sql, i) ?? scanMultiLineCommentMySQL(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? plainToken(i);
409
423
  }
410
424
  function scanTokenSQLite(sql, i) {
411
425
  return scanSingleLineComment(sql, i) ?? scanMultiLineComment(sql, i) ?? scanSingleQuotedString(sql, i) ?? scanDoubleQuotedString(sql, i) ?? scanBacktickQuotedIdentifier(sql, i) ?? scanBracketQuotedIdentifier(sql, i) ?? plainToken(i);
@@ -8,7 +8,7 @@ import {
8
8
  getDefaultPortForType,
9
9
  parseConnectionInfoFromDSN,
10
10
  stripCommentsAndStrings
11
- } from "./chunk-C7WEAPX4.js";
11
+ } from "./chunk-JEZZN2YZ.js";
12
12
 
13
13
  // src/tools/builtin-tools.ts
14
14
  var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
@@ -733,6 +733,21 @@ function resolveHost() {
733
733
  }
734
734
  return { host: "0.0.0.0", source: "default" };
735
735
  }
736
+ function resolveAllowedHosts() {
737
+ const args = parseCommandLineArgs();
738
+ const cliValue = requireFlagValue("allowed-hosts", args, "db.internal,app.example.com");
739
+ if (cliValue !== void 0) {
740
+ return { hosts: splitHostList(cliValue), source: "command line argument" };
741
+ }
742
+ const envValue = process.env.DBHUB_ALLOWED_HOSTS?.trim();
743
+ if (envValue) {
744
+ return { hosts: splitHostList(envValue), source: "environment variable" };
745
+ }
746
+ return { hosts: [], source: "default" };
747
+ }
748
+ function splitHostList(value) {
749
+ return value.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
750
+ }
736
751
  function redactDSN(dsn) {
737
752
  try {
738
753
  const url = new URL(dsn);
@@ -1787,9 +1802,6 @@ var ConnectorManager = class {
1787
1802
  if (source.query_timeout !== void 0 && connector.id !== "sqlite") {
1788
1803
  config.queryTimeoutSeconds = source.query_timeout;
1789
1804
  }
1790
- if (source.readonly !== void 0) {
1791
- config.readonly = source.readonly;
1792
- }
1793
1805
  if (source.search_path) {
1794
1806
  config.searchPath = source.search_path;
1795
1807
  }
@@ -2371,6 +2383,7 @@ export {
2371
2383
  resolveTransport,
2372
2384
  resolvePort,
2373
2385
  resolveHost,
2386
+ resolveAllowedHosts,
2374
2387
  resolveSourceConfigs,
2375
2388
  BUILTIN_TOOL_EXECUTE_SQL,
2376
2389
  BUILTIN_TOOL_SEARCH_OBJECTS,
package/dist/index.js CHANGED
@@ -9,12 +9,13 @@ import {
9
9
  isDemoMode,
10
10
  loadTomlConfig,
11
11
  mapArgumentsToArray,
12
+ resolveAllowedHosts,
12
13
  resolveHost,
13
14
  resolvePort,
14
15
  resolveSourceConfigs,
15
16
  resolveTomlConfigPath,
16
17
  resolveTransport
17
- } from "./chunk-DN45LGY4.js";
18
+ } from "./chunk-WPXPKRDU.js";
18
19
  import {
19
20
  loadConnectors
20
21
  } from "./chunk-WVVMH6FJ.js";
@@ -28,7 +29,7 @@ import {
28
29
  parseConnectionInfoFromDSN,
29
30
  splitSQLStatements,
30
31
  stripCommentsAndStrings
31
- } from "./chunk-C7WEAPX4.js";
32
+ } from "./chunk-JEZZN2YZ.js";
32
33
 
33
34
  // src/server.ts
34
35
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -133,6 +134,13 @@ var mutatingPatterns = {
133
134
  sqlserver: mutatingPattern
134
135
  };
135
136
  var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
137
+ var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
138
+ "table_info",
139
+ "index_info",
140
+ "index_list",
141
+ "foreign_key_list"
142
+ ]);
143
+ var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
136
144
  var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
137
145
  function isReadOnlySQL(sql, connectorType) {
138
146
  return checkReadOnly(
@@ -155,6 +163,15 @@ function checkReadOnly(cleanedSQL, connectorType) {
155
163
  return false;
156
164
  }
157
165
  }
166
+ if (firstWord === "pragma" && connectorType === "sqlite") {
167
+ if (cleanedSQL.includes("=")) {
168
+ return false;
169
+ }
170
+ const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
171
+ if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
172
+ return false;
173
+ }
174
+ }
158
175
  if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
159
176
  return false;
160
177
  }
@@ -1490,22 +1507,78 @@ Detected change in ${configPath}, reloading configuration...`);
1490
1507
  }
1491
1508
 
1492
1509
  // src/utils/cross-origin.ts
1510
+ import os from "node:os";
1493
1511
  var INVALID_HOST_CHARS = /[\s/\\@?#]/;
1494
- function validateOrigin(originHeader, hostHeader) {
1495
- if (originHeader === void 0) return { ok: true };
1496
- const trimmedOrigin = originHeader.trim();
1497
- if (!trimmedOrigin) {
1498
- return { ok: false, status: 400, message: "Malformed Origin header" };
1512
+ var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
1513
+ var ALLOW_ANY_HOST = "*";
1514
+ function normalizeHost(raw) {
1515
+ const trimmed = raw.trim();
1516
+ if (!trimmed) return null;
1517
+ if (trimmed === ALLOW_ANY_HOST) return ALLOW_ANY_HOST;
1518
+ if (INVALID_HOST_CHARS.test(trimmed)) return null;
1519
+ try {
1520
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
1521
+ return hostname || null;
1522
+ } catch {
1523
+ return null;
1499
1524
  }
1500
- const trimmedHost = (hostHeader ?? "").trim();
1501
- if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
1502
- return { ok: false, status: 400, message: "Malformed Host header" };
1525
+ }
1526
+ function getSelfHosts() {
1527
+ const hosts = [];
1528
+ try {
1529
+ const hostname = os.hostname().trim();
1530
+ if (hostname) hosts.push(hostname);
1531
+ } catch {
1503
1532
  }
1504
- let originHostname;
1533
+ let interfaces = {};
1505
1534
  try {
1506
- originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
1535
+ interfaces = os.networkInterfaces();
1507
1536
  } catch {
1508
- return { ok: false, status: 400, message: "Malformed Origin header" };
1537
+ }
1538
+ for (const addrs of Object.values(interfaces)) {
1539
+ for (const addr of addrs ?? []) {
1540
+ if (!addr.address || addr.internal) continue;
1541
+ const isIPv6 = addr.family === "IPv6" || addr.family === 6;
1542
+ if (isIPv6) {
1543
+ const bare = addr.address.split("%")[0];
1544
+ if (bare.toLowerCase().startsWith("fe80")) continue;
1545
+ hosts.push(`[${bare}]`);
1546
+ } else {
1547
+ hosts.push(addr.address);
1548
+ }
1549
+ }
1550
+ }
1551
+ return hosts;
1552
+ }
1553
+ function buildAllowedHosts(configured = [], bindHost, selfHosts = []) {
1554
+ const normalizedConfigured = configured.map(normalizeHost).filter((h) => h !== null);
1555
+ if (normalizedConfigured.includes(ALLOW_ANY_HOST)) {
1556
+ return /* @__PURE__ */ new Set([ALLOW_ANY_HOST]);
1557
+ }
1558
+ const hosts = /* @__PURE__ */ new Set();
1559
+ for (const h of LOOPBACK_HOSTS) {
1560
+ const normalized = normalizeHost(h);
1561
+ if (normalized) hosts.add(normalized);
1562
+ }
1563
+ const normalizedBind = bindHost ? normalizeHost(bindHost) : null;
1564
+ const bindIsWildcard = !normalizedBind || normalizedBind === "0.0.0.0" || normalizedBind === "[::]";
1565
+ if (normalizedBind && !bindIsWildcard) {
1566
+ hosts.add(normalizedBind);
1567
+ }
1568
+ if (bindIsWildcard) {
1569
+ for (const h of selfHosts) {
1570
+ const normalized = normalizeHost(h);
1571
+ if (normalized) hosts.add(normalized);
1572
+ }
1573
+ }
1574
+ for (const h of normalizedConfigured) hosts.add(h);
1575
+ return hosts;
1576
+ }
1577
+ function validateOrigin(originHeader, hostHeader, allowedHosts) {
1578
+ const allowAny = allowedHosts.has(ALLOW_ANY_HOST);
1579
+ const trimmedHost = (hostHeader ?? "").trim();
1580
+ if (!trimmedHost || INVALID_HOST_CHARS.test(trimmedHost)) {
1581
+ return { ok: false, status: 400, message: "Malformed Host header" };
1509
1582
  }
1510
1583
  let hostname;
1511
1584
  try {
@@ -1516,11 +1589,32 @@ function validateOrigin(originHeader, hostHeader) {
1516
1589
  if (!hostname) {
1517
1590
  return { ok: false, status: 400, message: "Malformed Host header" };
1518
1591
  }
1519
- if (originHostname !== hostname) {
1592
+ if (!allowAny && !allowedHosts.has(hostname)) {
1593
+ return {
1594
+ ok: false,
1595
+ status: 403,
1596
+ message: `Host '${hostname}' is not allowed. Only loopback is permitted by default; set --allowed-hosts (or DBHUB_ALLOWED_HOSTS) to serve other hostnames. This protects against DNS rebinding.`
1597
+ };
1598
+ }
1599
+ if (originHeader === void 0) return { ok: true };
1600
+ const trimmedOrigin = originHeader.trim();
1601
+ if (!trimmedOrigin) {
1602
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1603
+ }
1604
+ let originHostname;
1605
+ try {
1606
+ originHostname = new URL(trimmedOrigin).hostname.toLowerCase();
1607
+ } catch {
1608
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1609
+ }
1610
+ if (!originHostname) {
1611
+ return { ok: false, status: 400, message: "Malformed Origin header" };
1612
+ }
1613
+ if (!allowAny && !allowedHosts.has(originHostname)) {
1520
1614
  return {
1521
1615
  ok: false,
1522
1616
  status: 403,
1523
- message: "Origin does not match Host header"
1617
+ message: `Origin '${originHostname}' is not allowed`
1524
1618
  };
1525
1619
  }
1526
1620
  return { ok: true };
@@ -1578,7 +1672,7 @@ See documentation for more details on configuring database connections.
1578
1672
  const sources = sourceConfigsData.sources;
1579
1673
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1580
1674
  await connectorManager.connectWithSources(sources);
1581
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-OF5GPCBK.js");
1675
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-IPRZ55YC.js");
1582
1676
  initializeToolRegistry2({
1583
1677
  sources: sourceConfigsData.sources,
1584
1678
  tools: sourceConfigsData.tools
@@ -1599,6 +1693,7 @@ See documentation for more details on configuring database connections.
1599
1693
  const transportData = resolveTransport();
1600
1694
  const port = transportData.type === "http" ? resolvePort().port : null;
1601
1695
  const host = transportData.type === "http" ? resolveHost().host : null;
1696
+ const allowedHosts = transportData.type === "http" ? buildAllowedHosts(resolveAllowedHosts().hosts, host ?? void 0, getSelfHosts()) : /* @__PURE__ */ new Set();
1602
1697
  const activeModes = [];
1603
1698
  const modeDescriptions = [];
1604
1699
  const isDemo = isDemoMode();
@@ -1624,7 +1719,7 @@ See documentation for more details on configuring database connections.
1624
1719
  app.use(express.json());
1625
1720
  app.use((req, res, next) => {
1626
1721
  const origin = req.headers.origin;
1627
- const result = validateOrigin(origin, req.headers.host);
1722
+ const result = validateOrigin(origin, req.headers.host, allowedHosts);
1628
1723
  if (!result.ok) {
1629
1724
  return res.status(result.status).json({
1630
1725
  error: result.status === 400 ? "Bad Request" : "Forbidden",
@@ -1690,6 +1785,11 @@ See documentation for more details on configuring database connections.
1690
1785
  const displayHost = boundHost.includes(":") ? `[${boundHost}]` : boundHost;
1691
1786
  const userHost = boundHost === "0.0.0.0" || boundHost === "::" ? "localhost" : displayHost;
1692
1787
  console.error(`HTTP server listening on ${displayHost}:${boundPort}`);
1788
+ if (allowedHosts.has(ALLOW_ANY_HOST)) {
1789
+ console.error("Allowed hosts: * (DNS-rebinding protection DISABLED \u2014 ensure DBHub is fronted by your own auth/proxy)");
1790
+ } else {
1791
+ console.error(`Allowed hosts: ${[...allowedHosts].join(", ")} (set --allowed-hosts to serve other hostnames)`);
1792
+ }
1693
1793
  if (process.env.NODE_ENV === "development") {
1694
1794
  console.error("Development mode detected!");
1695
1795
  console.error(" Workbench dev server (with HMR): http://localhost:5173");
@@ -1726,11 +1826,11 @@ See documentation for more details on configuring database connections.
1726
1826
 
1727
1827
  // src/index.ts
1728
1828
  var connectorModules = [
1729
- { load: () => import("./postgres-EKVOBAH6.js"), name: "PostgreSQL", driver: "pg" },
1730
- { load: () => import("./sqlserver-5TFVBCMA.js"), name: "SQL Server", driver: "mssql" },
1731
- { load: () => import("./sqlite-2GBIF3MQ.js"), name: "SQLite", driver: "node:sqlite" },
1732
- { load: () => import("./mysql-WG6UFALX.js"), name: "MySQL", driver: "mysql2" },
1733
- { load: () => import("./mariadb-ERB4D3RP.js"), name: "MariaDB", driver: "mariadb" }
1829
+ { load: () => import("./postgres-BKBCGDZS.js"), name: "PostgreSQL", driver: "pg" },
1830
+ { load: () => import("./sqlserver-47APXMSF.js"), name: "SQL Server", driver: "mssql" },
1831
+ { load: () => import("./sqlite-CQ4LCBB6.js"), name: "SQLite", driver: "node:sqlite" },
1832
+ { load: () => import("./mysql-EK6TCER5.js"), name: "MySQL", driver: "mysql2" },
1833
+ { load: () => import("./mariadb-JPEKVFEW.js"), name: "MariaDB", driver: "mariadb" }
1734
1834
  ];
1735
1835
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1736
1836
  console.error("Fatal error:", error);
@@ -7,13 +7,13 @@ import {
7
7
  } from "./chunk-JFWX35TB.js";
8
8
  import {
9
9
  SQLRowLimiter
10
- } from "./chunk-BRXZ5ZQB.js";
10
+ } from "./chunk-BXXHWEQS.js";
11
11
  import {
12
12
  ConnectorRegistry,
13
13
  SafeURL,
14
14
  obfuscateDSNPassword,
15
15
  splitSQLStatements
16
- } from "./chunk-C7WEAPX4.js";
16
+ } from "./chunk-JEZZN2YZ.js";
17
17
 
18
18
  // src/connectors/mariadb/index.ts
19
19
  import * as mariadb from "mariadb";
@@ -466,6 +466,9 @@ var MariaDBConnector = class _MariaDBConnector {
466
466
  }
467
467
  const conn = await this.pool.getConnection();
468
468
  try {
469
+ if (options.readonly) {
470
+ await conn.query("START TRANSACTION READ ONLY");
471
+ }
469
472
  let processedSQL = sql;
470
473
  if (options.maxRows) {
471
474
  const statements = splitSQLStatements(sql, "mariadb");
@@ -492,8 +495,17 @@ var MariaDBConnector = class _MariaDBConnector {
492
495
  }
493
496
  const rows = parseQueryResults(results);
494
497
  const rowCount = extractAffectedRows(results);
498
+ if (options.readonly) {
499
+ await conn.query("COMMIT");
500
+ }
495
501
  return { rows, rowCount };
496
502
  } catch (error) {
503
+ if (options.readonly) {
504
+ try {
505
+ await conn.query("ROLLBACK");
506
+ } catch {
507
+ }
508
+ }
497
509
  console.error("Error executing query:", error);
498
510
  throw error;
499
511
  } finally {
@@ -7,13 +7,13 @@ import {
7
7
  } from "./chunk-JFWX35TB.js";
8
8
  import {
9
9
  SQLRowLimiter
10
- } from "./chunk-BRXZ5ZQB.js";
10
+ } from "./chunk-BXXHWEQS.js";
11
11
  import {
12
12
  ConnectorRegistry,
13
13
  SafeURL,
14
14
  obfuscateDSNPassword,
15
15
  splitSQLStatements
16
- } from "./chunk-C7WEAPX4.js";
16
+ } from "./chunk-JEZZN2YZ.js";
17
17
 
18
18
  // src/connectors/mysql/index.ts
19
19
  import mysql from "mysql2/promise";
@@ -472,6 +472,9 @@ var MySQLConnector = class _MySQLConnector {
472
472
  }
473
473
  const conn = await this.pool.getConnection();
474
474
  try {
475
+ if (options.readonly) {
476
+ await conn.query("START TRANSACTION READ ONLY");
477
+ }
475
478
  let processedSQL = sql;
476
479
  if (options.maxRows) {
477
480
  const statements = splitSQLStatements(sql, "mysql");
@@ -499,8 +502,17 @@ var MySQLConnector = class _MySQLConnector {
499
502
  const [firstResult] = results;
500
503
  const rows = parseQueryResults(firstResult);
501
504
  const rowCount = extractAffectedRows(firstResult);
505
+ if (options.readonly) {
506
+ await conn.query("COMMIT");
507
+ }
502
508
  return { rows, rowCount };
503
509
  } catch (error) {
510
+ if (options.readonly) {
511
+ try {
512
+ await conn.query("ROLLBACK");
513
+ } catch {
514
+ }
515
+ }
504
516
  console.error("Error executing query:", error);
505
517
  throw error;
506
518
  } finally {
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-JFWX35TB.js";
4
4
  import {
5
5
  SQLRowLimiter
6
- } from "./chunk-BRXZ5ZQB.js";
6
+ } from "./chunk-BXXHWEQS.js";
7
7
  import {
8
8
  ConnectorRegistry,
9
9
  SafeURL,
10
10
  obfuscateDSNPassword,
11
11
  splitSQLStatements
12
- } from "./chunk-C7WEAPX4.js";
12
+ } from "./chunk-JEZZN2YZ.js";
13
13
 
14
14
  // src/connectors/postgres/index.ts
15
15
  import fs from "fs";
@@ -491,6 +491,25 @@ var PostgresConnector = class _PostgresConnector {
491
491
  const statements = splitSQLStatements(sql, "postgres");
492
492
  if (statements.length === 1) {
493
493
  const processedStatement = SQLRowLimiter.applyMaxRows(statements[0], options.maxRows);
494
+ if (options.readonly) {
495
+ await client.query("BEGIN READ ONLY");
496
+ try {
497
+ const result2 = parameters && parameters.length > 0 ? await client.query(processedStatement, parameters) : await client.query(processedStatement);
498
+ await client.query("COMMIT");
499
+ return { rows: result2.rows, rowCount: result2.rowCount ?? result2.rows.length };
500
+ } catch (error) {
501
+ try {
502
+ await client.query("ROLLBACK");
503
+ } catch {
504
+ }
505
+ console.error(`[PostgreSQL executeSQL] ERROR: ${error.message}`);
506
+ console.error(`[PostgreSQL executeSQL] SQL: ${processedStatement}`);
507
+ if (parameters && parameters.length > 0) {
508
+ console.error(`[PostgreSQL executeSQL] Parameters: ${JSON.stringify(parameters)}`);
509
+ }
510
+ throw error;
511
+ }
512
+ }
494
513
  let result;
495
514
  if (parameters && parameters.length > 0) {
496
515
  try {
@@ -511,7 +530,7 @@ var PostgresConnector = class _PostgresConnector {
511
530
  }
512
531
  let allRows = [];
513
532
  let totalRowCount = 0;
514
- await client.query("BEGIN");
533
+ await client.query(options.readonly ? "BEGIN READ ONLY" : "BEGIN");
515
534
  try {
516
535
  for (let statement of statements) {
517
536
  const processedStatement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
@@ -525,7 +544,10 @@ var PostgresConnector = class _PostgresConnector {
525
544
  }
526
545
  await client.query("COMMIT");
527
546
  } catch (error) {
528
- await client.query("ROLLBACK");
547
+ try {
548
+ await client.query("ROLLBACK");
549
+ } catch {
550
+ }
529
551
  throw error;
530
552
  }
531
553
  return { rows: allRows, rowCount: totalRowCount };
@@ -2,9 +2,9 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-DN45LGY4.js";
5
+ } from "./chunk-WPXPKRDU.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
- import "./chunk-C7WEAPX4.js";
7
+ import "./chunk-JEZZN2YZ.js";
8
8
  export {
9
9
  ToolRegistry,
10
10
  getToolRegistry,
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-JFWX35TB.js";
4
4
  import {
5
5
  SQLRowLimiter
6
- } from "./chunk-BRXZ5ZQB.js";
6
+ } from "./chunk-BXXHWEQS.js";
7
7
  import {
8
8
  ConnectorRegistry,
9
9
  SafeURL,
10
10
  obfuscateDSNPassword,
11
11
  splitSQLStatements
12
- } from "./chunk-C7WEAPX4.js";
12
+ } from "./chunk-JEZZN2YZ.js";
13
13
 
14
14
  // src/connectors/sqlite/suppress-experimental-warning.ts
15
15
  var installed = false;
@@ -319,6 +319,9 @@ var SQLiteConnector = class _SQLiteConnector {
319
319
  if (!this.db) {
320
320
  throw new Error("Not connected to SQLite database");
321
321
  }
322
+ if (options.readonly) {
323
+ this.db.exec("PRAGMA query_only = ON");
324
+ }
322
325
  try {
323
326
  const statements = splitSQLStatements(sql, "sqlite");
324
327
  if (statements.length === 1) {
@@ -375,19 +378,30 @@ var SQLiteConnector = class _SQLiteConnector {
375
378
  }
376
379
  let totalChanges = 0;
377
380
  for (const statement of writeStatements) {
381
+ if (options.readonly) {
382
+ this.db.exec("PRAGMA query_only = ON");
383
+ }
378
384
  const result = this.prepare(statement).run();
379
385
  totalChanges += Number(result.changes);
380
386
  }
381
387
  let allRows = [];
382
388
  for (let statement of readStatements) {
389
+ if (options.readonly) {
390
+ this.db.exec("PRAGMA query_only = ON");
391
+ }
383
392
  statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
384
393
  const result = this.prepare(statement).all();
385
394
  allRows.push(...result);
386
395
  }
387
396
  return { rows: allRows, rowCount: totalChanges + allRows.length };
388
397
  }
389
- } catch (error) {
390
- throw error;
398
+ } finally {
399
+ if (options.readonly) {
400
+ try {
401
+ this.db.exec("PRAGMA query_only = OFF");
402
+ } catch {
403
+ }
404
+ }
391
405
  }
392
406
  }
393
407
  };
@@ -3,13 +3,13 @@ import {
3
3
  } from "./chunk-WVVMH6FJ.js";
4
4
  import {
5
5
  SQLRowLimiter
6
- } from "./chunk-BRXZ5ZQB.js";
6
+ } from "./chunk-BXXHWEQS.js";
7
7
  import {
8
8
  ConnectorRegistry,
9
9
  SafeURL,
10
10
  obfuscateDSNPassword,
11
11
  stripCommentsAndStrings
12
- } from "./chunk-C7WEAPX4.js";
12
+ } from "./chunk-JEZZN2YZ.js";
13
13
 
14
14
  // src/connectors/sqlserver/index.ts
15
15
  import sql from "mssql";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.22.4",
3
+ "version": "0.22.6",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
5
  "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
6
6
  "repository": {