@bytebase/dbhub 1.2.5 → 1.3.0

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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>DBHub - Minimal Database MCP Server</title>
8
- <script type="module" crossorigin src="/assets/index-BdBaFSHd.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BRlIJaqn.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BJ-1UrcV.css">
10
10
  </head>
11
11
  <body>
@@ -2,9 +2,9 @@ import {
2
2
  ToolRegistry,
3
3
  getToolRegistry,
4
4
  initializeToolRegistry
5
- } from "./chunk-VDA5BVNE.js";
5
+ } from "./chunk-77BJNE6E.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
- import "./chunk-MF6LSVWK.js";
7
+ import "./chunk-IXC4NQGC.js";
8
8
  export {
9
9
  ToolRegistry,
10
10
  getToolRegistry,
@@ -1,16 +1,18 @@
1
1
  import {
2
2
  quoteIdentifier
3
- } from "./chunk-JFWX35TB.js";
3
+ } from "./chunk-X2EC47W4.js";
4
4
  import {
5
5
  SQLRowLimiter,
6
6
  closeQuietly
7
- } from "./chunk-6QETXS44.js";
7
+ } from "./chunk-YIGJHEU6.js";
8
+ import "./chunk-ABTVMHD6.js";
8
9
  import {
9
10
  ConnectorRegistry,
10
11
  SafeURL,
11
12
  obfuscateDSNPassword,
12
- splitSQLStatements
13
- } from "./chunk-MF6LSVWK.js";
13
+ splitSQLStatements,
14
+ stripCommentsAndStrings
15
+ } from "./chunk-IXC4NQGC.js";
14
16
 
15
17
  // src/connectors/sqlite/suppress-experimental-warning.ts
16
18
  var installed = false;
@@ -321,6 +323,16 @@ var SQLiteConnector = class _SQLiteConnector {
321
323
  "SQLite does not support stored procedures. Functions are defined programmatically through the SQLite API, not stored in the database."
322
324
  );
323
325
  }
326
+ /**
327
+ * Whether a statement returns rows (and so must run via `all()` rather than
328
+ * `run()`). Classified on the comment/string-stripped text so a leading
329
+ * attribution comment cannot hide a SELECT, matching the row limiter and the
330
+ * read-only classifier.
331
+ */
332
+ returnsRows(statement) {
333
+ const stripped = stripCommentsAndStrings(statement, "sqlite").toLowerCase().trim();
334
+ return stripped.startsWith("select") || stripped.startsWith("with") || stripped.startsWith("explain") || stripped.startsWith("analyze") || stripped.startsWith("pragma") && (stripped.includes("table_info") || stripped.includes("index_info") || stripped.includes("index_list") || stripped.includes("foreign_key_list"));
335
+ }
324
336
  async executeSQL(sql, options, parameters) {
325
337
  if (!this.db) {
326
338
  throw new Error("Not connected to SQLite database");
@@ -332,13 +344,13 @@ var SQLiteConnector = class _SQLiteConnector {
332
344
  const statements = splitSQLStatements(sql, "sqlite");
333
345
  if (statements.length === 1) {
334
346
  let processedStatement = statements[0];
335
- const trimmedStatement = statements[0].toLowerCase().trim();
336
- const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
347
+ const isReadStatement = this.returnsRows(statements[0]);
337
348
  let probeApplied = false;
338
349
  if (options.maxRows) {
339
350
  const rewrite = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
340
351
  processedStatement,
341
- options.maxRows
352
+ options.maxRows,
353
+ "sqlite"
342
354
  );
343
355
  processedStatement = rewrite.sql;
344
356
  probeApplied = rewrite.probeApplied;
@@ -366,8 +378,7 @@ var SQLiteConnector = class _SQLiteConnector {
366
378
  const readStatements = [];
367
379
  const writeStatements = [];
368
380
  for (const statement of statements) {
369
- const trimmedStatement = statement.toLowerCase().trim();
370
- if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
381
+ if (this.returnsRows(statement)) {
371
382
  readStatements.push(statement);
372
383
  } else {
373
384
  writeStatements.push(statement);
@@ -387,7 +398,8 @@ var SQLiteConnector = class _SQLiteConnector {
387
398
  }
388
399
  const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
389
400
  statement,
390
- options.maxRows
401
+ options.maxRows,
402
+ "sqlite"
391
403
  );
392
404
  const rows = this.prepare(processedStatement).all();
393
405
  const resultSet = { sql: statement, rows, rowCount: rows.length };
@@ -1,9 +1,3 @@
1
- import {
2
- sqlServerDynamicSqlKeywords,
3
- sqlServerDynamicSqlPattern,
4
- sqlServerPassThroughKeywords,
5
- sqlServerPassThroughPattern
6
- } from "./chunk-IQRBLAB4.js";
7
1
  import {
8
2
  isDriverNotInstalled
9
3
  } from "./chunk-WVVMH6FJ.js";
@@ -14,14 +8,21 @@ import {
14
8
  import {
15
9
  SQLRowLimiter,
16
10
  closeQuietly
17
- } from "./chunk-6QETXS44.js";
11
+ } from "./chunk-YIGJHEU6.js";
12
+ import {
13
+ sqlServerDynamicSqlKeywords,
14
+ sqlServerDynamicSqlPattern,
15
+ sqlServerPassThroughKeywords,
16
+ sqlServerPassThroughPattern
17
+ } from "./chunk-ABTVMHD6.js";
18
18
  import {
19
19
  ConnectorRegistry,
20
+ LEADING_SQL_NOISE,
20
21
  SafeURL,
21
22
  obfuscateDSNPassword,
22
23
  splitSQLStatements,
23
24
  stripCommentsAndStrings
24
- } from "./chunk-MF6LSVWK.js";
25
+ } from "./chunk-IXC4NQGC.js";
25
26
 
26
27
  // src/connectors/sqlserver/index.ts
27
28
  import sql from "mssql";
@@ -551,9 +552,7 @@ var _SQLServerConnector = class _SQLServerConnector {
551
552
  if (!this.connection) {
552
553
  throw new Error("Not connected to SQL Server database");
553
554
  }
554
- const afterNoise = sqlQuery.slice(
555
- sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
556
- );
555
+ const afterNoise = sqlQuery.replace(LEADING_SQL_NOISE, "");
557
556
  if (/^explain\b/i.test(afterNoise)) {
558
557
  const { analyze, query } = _SQLServerConnector.parseExplainPrefix(
559
558
  afterNoise.slice("explain".length).trim()
@@ -1013,13 +1012,6 @@ var _SQLServerConnector = class _SQLServerConnector {
1013
1012
  return null;
1014
1013
  }
1015
1014
  };
1016
- /**
1017
- * Leading whitespace and SQL comments to skip before looking for a keyword.
1018
- * The read-only validator strips comments before checking the first keyword,
1019
- * so the connector must skip them too; otherwise an EXPLAIN preceded by a
1020
- * comment passes validation but reaches the server untranslated.
1021
- */
1022
- _SQLServerConnector.LEADING_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
1023
1015
  /** Boolean spellings PostgreSQL accepts for an EXPLAIN option. */
1024
1016
  _SQLServerConnector.EXPLAIN_ON = /^(?:true|on|1)$/i;
1025
1017
  _SQLServerConnector.EXPLAIN_OFF = /^(?:false|off|0)$/i;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
4
4
  "mcpName": "io.github.bytebase/dbhub",
5
- "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, SQLite, MariaDB",
5
+ "description": "Minimal, token-efficient Database MCP Server for PostgreSQL, MySQL, SQL Server, Oracle, SQLite, MariaDB",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/bytebase/dbhub.git"
@@ -61,6 +61,7 @@
61
61
  "mariadb": "^3.4.0",
62
62
  "mssql": "^11.0.1",
63
63
  "mysql2": "^3.13.0",
64
+ "oracledb": "^7.0.1",
64
65
  "pg": "^8.13.3"
65
66
  },
66
67
  "devDependencies": {
@@ -69,10 +70,12 @@
69
70
  "@testcontainers/mariadb": "^11.0.3",
70
71
  "@testcontainers/mssqlserver": "^11.0.3",
71
72
  "@testcontainers/mysql": "^11.0.3",
73
+ "@testcontainers/oraclefree": "^11.14.0",
72
74
  "@testcontainers/postgresql": "^11.0.3",
73
75
  "@types/express": "^4.17.21",
74
76
  "@types/mssql": "^9.1.7",
75
77
  "@types/node": "^22.13.10",
78
+ "@types/oracledb": "^7.0.3",
76
79
  "@types/pg": "^8.11.11",
77
80
  "@types/ssh2": "^1.15.5",
78
81
  "concurrently": "^9.2.1",
@@ -1,254 +0,0 @@
1
- import {
2
- blankCommentsAndStrings,
3
- stripCommentsAndStrings
4
- } from "./chunk-MF6LSVWK.js";
5
-
6
- // src/utils/sql-row-limiter.ts
7
- var SQLRowLimiter = class {
8
- /**
9
- * Check if a SQL statement is a SELECT query that can benefit from row limiting
10
- * Only handles SELECT queries
11
- */
12
- static isSelectQuery(sql) {
13
- const trimmed = sql.trim().toLowerCase();
14
- return trimmed.startsWith("select");
15
- }
16
- /**
17
- * Check if a SQL statement already has a LIMIT clause.
18
- * Strips comments and string literals first to avoid false positives.
19
- */
20
- static hasLimitClause(sql) {
21
- const cleanedSQL = stripCommentsAndStrings(sql);
22
- const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
23
- return limitRegex.test(cleanedSQL);
24
- }
25
- /**
26
- * Check if a SQL statement already has a TOP clause (SQL Server).
27
- * Strips comments and string literals first to avoid false positives.
28
- */
29
- static hasTopClause(sql) {
30
- const cleanedSQL = stripCommentsAndStrings(sql);
31
- const topRegex = /\bselect\s+top\s+\d+/i;
32
- return topRegex.test(cleanedSQL);
33
- }
34
- /**
35
- * Extract existing LIMIT value from SQL if present.
36
- * Strips comments and string literals first to avoid false positives.
37
- */
38
- static extractLimitValue(sql) {
39
- const cleanedSQL = stripCommentsAndStrings(sql);
40
- const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
41
- if (limitMatch) {
42
- return parseInt(limitMatch[1], 10);
43
- }
44
- return null;
45
- }
46
- /**
47
- * Extract existing TOP value from SQL if present (SQL Server).
48
- * Strips comments and string literals first to avoid false positives.
49
- */
50
- static extractTopValue(sql) {
51
- const cleanedSQL = stripCommentsAndStrings(sql);
52
- const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
53
- if (topMatch) {
54
- return parseInt(topMatch[1], 10);
55
- }
56
- return null;
57
- }
58
- /**
59
- * Add or modify LIMIT clause in a SQL statement
60
- */
61
- static applyLimitToQuery(sql, maxRows) {
62
- const existingLimit = this.extractLimitValue(sql);
63
- if (existingLimit !== null) {
64
- const effectiveLimit = Math.min(existingLimit, maxRows);
65
- return sql.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
66
- } else {
67
- const trimmed = sql.trim();
68
- const hasSemicolon = trimmed.endsWith(";");
69
- const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
70
- return `${sqlWithoutSemicolon}
71
- LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
72
- }
73
- }
74
- /**
75
- * Scan blanked (comment/string-free, length-preserving) SQL for parenthesis
76
- * depth, invoking onMatch for every regex hit at depth 0 (i.e. not nested
77
- * inside a subquery, function call, or window OVER clause).
78
- */
79
- static scanTopLevel(blankedSQL, regex, onMatch) {
80
- let depth = 0;
81
- let m;
82
- while ((m = regex.exec(blankedSQL)) !== null) {
83
- const token = m[0];
84
- if (token === "(") {
85
- depth++;
86
- } else if (token === ")") {
87
- depth--;
88
- } else if (depth === 0) {
89
- onMatch(m);
90
- }
91
- }
92
- }
93
- /**
94
- * Check if a SQL statement combines multiple SELECTs with a set operator
95
- * (UNION [ALL], INTERSECT, EXCEPT) at the top level — i.e. not nested
96
- * inside a subquery already wrapped in parentheses. Strips comments and
97
- * string literals first to avoid false positives.
98
- */
99
- static hasSetOperator(sql) {
100
- const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
101
- let found = false;
102
- this.scanTopLevel(blankedSQL, /\(|\)|\bunion\b|\bintersect\b|\bexcept\b/gi, () => {
103
- found = true;
104
- });
105
- return found;
106
- }
107
- /**
108
- * Find the start index of a top-level trailing ORDER BY clause (not one
109
- * nested inside a subquery or a window function's OVER (...) clause).
110
- * Returns -1 if none exists. Operates on the original (non-blanked) SQL
111
- * length, since blankCommentsAndStrings preserves length/position.
112
- */
113
- static findTopLevelOrderByIndex(sql) {
114
- const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
115
- let lastIndex = -1;
116
- this.scanTopLevel(blankedSQL, /\(|\)|\border\s+by\b/gi, (m) => {
117
- lastIndex = m.index;
118
- });
119
- return lastIndex;
120
- }
121
- /**
122
- * Add or modify TOP clause in a SQL statement (SQL Server)
123
- */
124
- static applyTopToQuery(sql, maxRows) {
125
- if (this.hasSetOperator(sql)) {
126
- const trimmed = sql.trim();
127
- const hasSemicolon = trimmed.endsWith(";");
128
- const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
129
- const orderByIndex = this.findTopLevelOrderByIndex(sqlWithoutSemicolon);
130
- if (orderByIndex !== -1) {
131
- const innerSql = sqlWithoutSemicolon.slice(0, orderByIndex).trimEnd();
132
- const orderByClause = sqlWithoutSemicolon.slice(orderByIndex).trim();
133
- return `SELECT TOP ${maxRows} * FROM (${innerSql}
134
- ) AS subq ${orderByClause}${hasSemicolon ? ";" : ""}`;
135
- }
136
- return `SELECT TOP ${maxRows} * FROM (${sqlWithoutSemicolon}
137
- ) AS subq${hasSemicolon ? ";" : ""}`;
138
- }
139
- const existingTop = this.extractTopValue(sql);
140
- if (existingTop !== null) {
141
- const effectiveTop = Math.min(existingTop, maxRows);
142
- return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
143
- }
144
- return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
145
- }
146
- /**
147
- * Check if a LIMIT clause uses a parameter placeholder (not a literal number).
148
- * Strips comments and string literals first to avoid false positives.
149
- */
150
- static hasParameterizedLimit(sql) {
151
- const cleanedSQL = stripCommentsAndStrings(sql);
152
- const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
153
- return parameterizedLimitRegex.test(cleanedSQL);
154
- }
155
- /**
156
- * Apply maxRows limit to a SELECT query only
157
- *
158
- * This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
159
- * the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
160
- *
161
- * For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
162
- * to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
163
- */
164
- static applyMaxRows(sql, maxRows) {
165
- if (!maxRows || !this.isSelectQuery(sql)) {
166
- return sql;
167
- }
168
- if (this.hasParameterizedLimit(sql)) {
169
- const trimmed = sql.trim();
170
- const hasSemicolon = trimmed.endsWith(";");
171
- const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
172
- return `SELECT * FROM (${sqlWithoutSemicolon}
173
- ) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
174
- }
175
- return this.applyLimitToQuery(sql, maxRows);
176
- }
177
- /**
178
- * Apply maxRows limit to a SELECT query using SQL Server TOP syntax
179
- */
180
- static applyMaxRowsForSQLServer(sql, maxRows) {
181
- if (!maxRows || !this.isSelectQuery(sql)) {
182
- return sql;
183
- }
184
- return this.applyTopToQuery(sql, maxRows);
185
- }
186
- /**
187
- * Like applyMaxRows, but requests one probe row (maxRows + 1) whenever the
188
- * cap is the binding constraint, so callers can detect truncation exactly
189
- * instead of guessing from `rowCount === maxRows` (which a table with
190
- * exactly maxRows rows also produces).
191
- *
192
- * `probeApplied` is false when the cap cannot fire: no maxRows, not a
193
- * SELECT, or the query's own literal LIMIT is already within the cap (the
194
- * user asked for fewer rows than the cap — that is their limit, not
195
- * truncation).
196
- */
197
- static applyMaxRowsWithTruncationProbe(sql, maxRows) {
198
- if (!maxRows || !this.isSelectQuery(sql)) {
199
- return { sql, probeApplied: false };
200
- }
201
- if (!this.hasParameterizedLimit(sql)) {
202
- const existingLimit = this.extractLimitValue(sql);
203
- if (existingLimit !== null && existingLimit <= maxRows) {
204
- return { sql, probeApplied: false };
205
- }
206
- }
207
- return { sql: this.applyMaxRows(sql, maxRows + 1), probeApplied: true };
208
- }
209
- /**
210
- * SQL Server variant of applyMaxRowsWithTruncationProbe, using TOP syntax.
211
- * A set-operator statement is always wrapped (any TOP on an individual
212
- * branch caps only that branch, not the combined output), so the probe
213
- * applies there regardless of branch-level TOPs.
214
- */
215
- static applyMaxRowsForSQLServerWithTruncationProbe(sql, maxRows) {
216
- if (!maxRows || !this.isSelectQuery(sql)) {
217
- return { sql, probeApplied: false };
218
- }
219
- if (!this.hasSetOperator(sql)) {
220
- const existingTop = this.extractTopValue(sql);
221
- if (existingTop !== null && existingTop <= maxRows) {
222
- return { sql, probeApplied: false };
223
- }
224
- }
225
- return { sql: this.applyMaxRowsForSQLServer(sql, maxRows + 1), probeApplied: true };
226
- }
227
- /**
228
- * Post-process a result set produced by a truncation-probe rewrite: when
229
- * the probe row came back (more than maxRows rows), drop it, clamp the
230
- * count to maxRows, and mark the set truncated so consumers can tell a
231
- * capped result from a complete one.
232
- */
233
- static flagTruncation(resultSet, maxRows, probeApplied) {
234
- if (!probeApplied || !maxRows || resultSet.rows.length <= maxRows) {
235
- return;
236
- }
237
- resultSet.rows = resultSet.rows.slice(0, maxRows);
238
- resultSet.rowCount = maxRows;
239
- resultSet.truncated = true;
240
- }
241
- };
242
-
243
- // src/utils/resource-cleanup.ts
244
- async function closeQuietly(close) {
245
- try {
246
- await close();
247
- } catch {
248
- }
249
- }
250
-
251
- export {
252
- SQLRowLimiter,
253
- closeQuietly
254
- };