@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.
@@ -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-DBYlgGks.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DOZ5HOZ9.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-GSSLHJB6.js";
5
+ } from "./chunk-2DFLBX4D.js";
6
6
  import "./chunk-WVVMH6FJ.js";
7
- import "./chunk-JEZZN2YZ.js";
7
+ import "./chunk-MF6LSVWK.js";
8
8
  export {
9
9
  ToolRegistry,
10
10
  getToolRegistry,
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  SQLRowLimiter,
6
6
  closeQuietly
7
- } from "./chunk-S2V6RZ3C.js";
7
+ } from "./chunk-6QETXS44.js";
8
8
  import {
9
9
  ConnectorRegistry,
10
10
  SafeURL,
11
11
  obfuscateDSNPassword,
12
12
  splitSQLStatements
13
- } from "./chunk-JEZZN2YZ.js";
13
+ } from "./chunk-MF6LSVWK.js";
14
14
 
15
15
  // src/connectors/sqlite/suppress-experimental-warning.ts
16
16
  var installed = false;
@@ -334,17 +334,20 @@ var SQLiteConnector = class _SQLiteConnector {
334
334
  let processedStatement = statements[0];
335
335
  const trimmedStatement = statements[0].toLowerCase().trim();
336
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"));
337
+ let probeApplied = false;
337
338
  if (options.maxRows) {
338
- processedStatement = SQLRowLimiter.applyMaxRows(processedStatement, options.maxRows);
339
+ const rewrite = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
340
+ processedStatement,
341
+ options.maxRows
342
+ );
343
+ processedStatement = rewrite.sql;
344
+ probeApplied = rewrite.probeApplied;
339
345
  }
340
346
  if (isReadStatement) {
341
- if (parameters && parameters.length > 0) {
342
- const rows = this.prepare(processedStatement).all(...parameters);
343
- return { rows, rowCount: rows.length };
344
- } else {
345
- const rows = this.prepare(processedStatement).all();
346
- return { rows, rowCount: rows.length };
347
- }
347
+ const rows = parameters && parameters.length > 0 ? this.prepare(processedStatement).all(...parameters) : this.prepare(processedStatement).all();
348
+ const resultSet = { sql: statements[0], rows, rowCount: rows.length };
349
+ SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
350
+ return { resultSets: [resultSet] };
348
351
  } else {
349
352
  let result;
350
353
  if (parameters && parameters.length > 0) {
@@ -352,7 +355,9 @@ var SQLiteConnector = class _SQLiteConnector {
352
355
  } else {
353
356
  result = this.prepare(processedStatement).run();
354
357
  }
355
- return { rows: [], rowCount: Number(result.changes) };
358
+ return {
359
+ resultSets: [{ sql: statements[0], rows: [], rowCount: Number(result.changes) }]
360
+ };
356
361
  }
357
362
  } else {
358
363
  if (parameters && parameters.length > 0) {
@@ -368,24 +373,28 @@ var SQLiteConnector = class _SQLiteConnector {
368
373
  writeStatements.push(statement);
369
374
  }
370
375
  }
371
- let totalChanges = 0;
376
+ const resultSets = [];
372
377
  for (const statement of writeStatements) {
373
378
  if (options.readonly) {
374
379
  this.db.exec("PRAGMA query_only = ON");
375
380
  }
376
381
  const result = this.prepare(statement).run();
377
- totalChanges += Number(result.changes);
382
+ resultSets.push({ sql: statement, rows: [], rowCount: Number(result.changes) });
378
383
  }
379
- let allRows = [];
380
- for (let statement of readStatements) {
384
+ for (const statement of readStatements) {
381
385
  if (options.readonly) {
382
386
  this.db.exec("PRAGMA query_only = ON");
383
387
  }
384
- statement = SQLRowLimiter.applyMaxRows(statement, options.maxRows);
385
- const result = this.prepare(statement).all();
386
- allRows.push(...result);
388
+ const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
389
+ statement,
390
+ options.maxRows
391
+ );
392
+ const rows = this.prepare(processedStatement).all();
393
+ const resultSet = { sql: statement, rows, rowCount: rows.length };
394
+ SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
395
+ resultSets.push(resultSet);
387
396
  }
388
- return { rows: allRows, rowCount: totalChanges + allRows.length };
397
+ return { resultSets };
389
398
  }
390
399
  } finally {
391
400
  if (options.readonly) {
@@ -3,7 +3,7 @@ import {
3
3
  sqlServerDynamicSqlPattern,
4
4
  sqlServerPassThroughKeywords,
5
5
  sqlServerPassThroughPattern
6
- } from "./chunk-EPOKWSFC.js";
6
+ } from "./chunk-IQRBLAB4.js";
7
7
  import {
8
8
  isDriverNotInstalled
9
9
  } from "./chunk-WVVMH6FJ.js";
@@ -14,13 +14,14 @@ 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
+ splitSQLStatements,
22
23
  stripCommentsAndStrings
23
- } from "./chunk-JEZZN2YZ.js";
24
+ } from "./chunk-MF6LSVWK.js";
24
25
 
25
26
  // src/connectors/sqlserver/index.ts
26
27
  import sql from "mssql";
@@ -561,11 +562,24 @@ var _SQLServerConnector = class _SQLServerConnector {
561
562
  }
562
563
  try {
563
564
  let processedSQL = sqlQuery;
565
+ let probeApplied = false;
564
566
  if (options.maxRows) {
565
- processedSQL = SQLRowLimiter.applyMaxRowsForSQLServer(sqlQuery, options.maxRows);
567
+ const rewrite = SQLRowLimiter.applyMaxRowsForSQLServerWithTruncationProbe(
568
+ sqlQuery,
569
+ options.maxRows
570
+ );
571
+ processedSQL = rewrite.sql;
572
+ probeApplied = rewrite.probeApplied;
566
573
  }
574
+ const isSingleStatement = splitSQLStatements(processedSQL, "sqlserver").length === 1;
567
575
  if (options.readonly) {
568
- return await this.executeReadOnly(processedSQL, parameters);
576
+ return await this.executeReadOnly(
577
+ processedSQL,
578
+ parameters,
579
+ isSingleStatement ? sqlQuery : void 0,
580
+ options.maxRows,
581
+ probeApplied
582
+ );
569
583
  }
570
584
  const request = this.connection.request();
571
585
  const messages = [];
@@ -583,15 +597,62 @@ var _SQLServerConnector = class _SQLServerConnector {
583
597
  );
584
598
  _SQLServerConnector.bindParameters(request, parameters);
585
599
  const result = await request.query(processedSQL);
600
+ const resultSets = _SQLServerConnector.buildResultSets(
601
+ result.recordsets,
602
+ result.rowsAffected,
603
+ isSingleStatement ? sqlQuery : void 0
604
+ );
605
+ if (resultSets.length > 0) {
606
+ SQLRowLimiter.flagTruncation(resultSets[0], options.maxRows, probeApplied);
607
+ }
586
608
  return {
587
- rows: result.recordset || [],
588
- rowCount: result.rowsAffected[0] || 0,
609
+ resultSets,
589
610
  ...messages.length > 0 ? { messages } : {}
590
611
  };
591
612
  } catch (error) {
592
613
  throw new Error(`Failed to execute query: ${error.message}`);
593
614
  }
594
615
  }
616
+ /**
617
+ * Builds one result set per SELECT-producing statement in the batch, plus
618
+ * (if any) a trailing result set summarizing the write-only statements.
619
+ *
620
+ * node-mssql only pushes a `recordsets` entry for statements that produce
621
+ * columns (SELECT); INSERT/UPDATE/DELETE statements in the same batch
622
+ * contribute to `rowsAffected` only, with no way to recover which
623
+ * `rowsAffected` entry belongs to which statement from the driver's final
624
+ * arrays (`recordsets`/`rowsAffected` aren't index-aligned - see the
625
+ * tedious `doneHandler`, which always pushes to `rowsAffected` but only
626
+ * conditionally to `recordsets`). So a batch of pure writes collapses to a
627
+ * single summed result set, and a batch mixing writes with selects gets
628
+ * one result set per select plus one trailing "writes" set for the rest -
629
+ * not a truly per-statement breakdown, but no read statement's rows are
630
+ * ever merged with another's, which is what mattered for #380.
631
+ *
632
+ * `sourceSql`, when given, is attributed to the single resulting set - it
633
+ * must be omitted (pass `undefined`) unless the caller has already
634
+ * confirmed the batch is unambiguously one statement, since for a genuine
635
+ * multi-statement batch there's no reliable way to say which source
636
+ * statement a given recordset (or the trailing writes set) came from.
637
+ */
638
+ static buildResultSets(recordsets, rowsAffected, sourceSql) {
639
+ const sets = (recordsets ?? []).map((recordset) => {
640
+ const rows = recordset ?? [];
641
+ return { rows, rowCount: rows.length };
642
+ });
643
+ const totalAffected = (rowsAffected ?? []).reduce((total, count) => total + (count ?? 0), 0);
644
+ const accountedFor = sets.reduce((total, set) => total + set.rowCount, 0);
645
+ const writesOnly = totalAffected - accountedFor;
646
+ if (sets.length === 0) {
647
+ sets.push({ rows: [], rowCount: totalAffected });
648
+ } else if (writesOnly > 0) {
649
+ sets.push({ rows: [], rowCount: writesOnly });
650
+ }
651
+ if (sets.length === 1 && sourceSql !== void 0) {
652
+ sets[0].sql = sourceSql;
653
+ }
654
+ return sets;
655
+ }
595
656
  /**
596
657
  * Bind positional parameters as @p1, @p2, ... inferring the SQL Server type
597
658
  * from each JavaScript value. Shared by every execution path so they cannot
@@ -673,7 +734,7 @@ var _SQLServerConnector = class _SQLServerConnector {
673
734
  * Dangerous constructs are rejected before the transaction opens; see
674
735
  * assertNoReadOnlyEscapes.
675
736
  */
676
- async executeReadOnly(processedSQL, parameters) {
737
+ async executeReadOnly(processedSQL, parameters, sourceSql, maxRows, probeApplied) {
677
738
  this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
678
739
  const transaction = new sql.Transaction(this.connection);
679
740
  await transaction.begin();
@@ -709,9 +770,16 @@ var _SQLServerConnector = class _SQLServerConnector {
709
770
  }
710
771
  }
711
772
  }
773
+ const resultSets = _SQLServerConnector.buildResultSets(
774
+ result.recordsets,
775
+ result.rowsAffected,
776
+ sourceSql
777
+ );
778
+ if (resultSets.length > 0) {
779
+ SQLRowLimiter.flagTruncation(resultSets[0], maxRows, probeApplied);
780
+ }
712
781
  return {
713
- rows: result.recordset || [],
714
- rowCount: result.rowsAffected[0] || 0,
782
+ resultSets,
715
783
  ...messages.length > 0 ? { messages } : {}
716
784
  };
717
785
  }
@@ -756,8 +824,12 @@ var _SQLServerConnector = class _SQLServerConnector {
756
824
  const planRow = planResult.recordset?.[0];
757
825
  const planXml = planRow ? Object.values(planRow)[0] : null;
758
826
  return {
759
- rows: planXml != null ? [{ plan: planXml }] : [],
760
- rowCount: planXml != null ? 1 : 0
827
+ resultSets: [
828
+ {
829
+ rows: planXml != null ? [{ plan: planXml }] : [],
830
+ rowCount: planXml != null ? 1 : 0
831
+ }
832
+ ]
761
833
  };
762
834
  } catch (error) {
763
835
  throw new Error(`Failed to explain query: ${error.message}`);
@@ -878,8 +950,12 @@ var _SQLServerConnector = class _SQLServerConnector {
878
950
  }
879
951
  const planXml = _SQLServerConnector.extractPlanXml(planResult);
880
952
  return {
881
- rows: planXml != null ? [{ plan: planXml }] : [],
882
- rowCount: planXml != null ? 1 : 0
953
+ resultSets: [
954
+ {
955
+ rows: planXml != null ? [{ plan: planXml }] : [],
956
+ rowCount: planXml != null ? 1 : 0
957
+ }
958
+ ]
883
959
  };
884
960
  } catch (error) {
885
961
  throw new Error(`Failed to explain analyze query: ${error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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": {
@@ -33,7 +33,9 @@
33
33
  "test:unit": "vitest run --project unit",
34
34
  "test:watch": "vitest",
35
35
  "test:integration": "vitest run --project integration",
36
- "test:build": "node scripts/smoke-test-build.mjs"
36
+ "test:build": "node scripts/smoke-test-build.mjs",
37
+ "sync-version": "node scripts/sync-version.mjs",
38
+ "version": "pnpm run sync-version && git add plugin/.claude-plugin/plugin.json plugin/.mcp.json server.json"
37
39
  },
38
40
  "engines": {
39
41
  "node": ">=22.5.0"
@@ -53,6 +55,7 @@
53
55
  "zod": "^4.2.0"
54
56
  },
55
57
  "optionalDependencies": {
58
+ "@aws-sdk/credential-providers": "^3.1001.0",
56
59
  "@aws-sdk/rds-signer": "^3.1001.0",
57
60
  "@azure/identity": "^4.8.0",
58
61
  "mariadb": "^3.4.0",
@@ -1,138 +0,0 @@
1
- import {
2
- stripCommentsAndStrings
3
- } from "./chunk-JEZZN2YZ.js";
4
-
5
- // src/utils/sql-row-limiter.ts
6
- var SQLRowLimiter = class {
7
- /**
8
- * Check if a SQL statement is a SELECT query that can benefit from row limiting
9
- * Only handles SELECT queries
10
- */
11
- static isSelectQuery(sql) {
12
- const trimmed = sql.trim().toLowerCase();
13
- return trimmed.startsWith("select");
14
- }
15
- /**
16
- * Check if a SQL statement already has a LIMIT clause.
17
- * Strips comments and string literals first to avoid false positives.
18
- */
19
- static hasLimitClause(sql) {
20
- const cleanedSQL = stripCommentsAndStrings(sql);
21
- const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
22
- return limitRegex.test(cleanedSQL);
23
- }
24
- /**
25
- * Check if a SQL statement already has a TOP clause (SQL Server).
26
- * Strips comments and string literals first to avoid false positives.
27
- */
28
- static hasTopClause(sql) {
29
- const cleanedSQL = stripCommentsAndStrings(sql);
30
- const topRegex = /\bselect\s+top\s+\d+/i;
31
- return topRegex.test(cleanedSQL);
32
- }
33
- /**
34
- * Extract existing LIMIT value from SQL if present.
35
- * Strips comments and string literals first to avoid false positives.
36
- */
37
- static extractLimitValue(sql) {
38
- const cleanedSQL = stripCommentsAndStrings(sql);
39
- const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
40
- if (limitMatch) {
41
- return parseInt(limitMatch[1], 10);
42
- }
43
- return null;
44
- }
45
- /**
46
- * Extract existing TOP value from SQL if present (SQL Server).
47
- * Strips comments and string literals first to avoid false positives.
48
- */
49
- static extractTopValue(sql) {
50
- const cleanedSQL = stripCommentsAndStrings(sql);
51
- const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
52
- if (topMatch) {
53
- return parseInt(topMatch[1], 10);
54
- }
55
- return null;
56
- }
57
- /**
58
- * Add or modify LIMIT clause in a SQL statement
59
- */
60
- static applyLimitToQuery(sql, maxRows) {
61
- const existingLimit = this.extractLimitValue(sql);
62
- if (existingLimit !== null) {
63
- const effectiveLimit = Math.min(existingLimit, maxRows);
64
- return sql.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
65
- } else {
66
- const trimmed = sql.trim();
67
- const hasSemicolon = trimmed.endsWith(";");
68
- const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
69
- return `${sqlWithoutSemicolon}
70
- LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
71
- }
72
- }
73
- /**
74
- * Add or modify TOP clause in a SQL statement (SQL Server)
75
- */
76
- static applyTopToQuery(sql, maxRows) {
77
- const existingTop = this.extractTopValue(sql);
78
- if (existingTop !== null) {
79
- const effectiveTop = Math.min(existingTop, maxRows);
80
- return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
81
- } else {
82
- return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
83
- }
84
- }
85
- /**
86
- * Check if a LIMIT clause uses a parameter placeholder (not a literal number).
87
- * Strips comments and string literals first to avoid false positives.
88
- */
89
- static hasParameterizedLimit(sql) {
90
- const cleanedSQL = stripCommentsAndStrings(sql);
91
- const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
92
- return parameterizedLimitRegex.test(cleanedSQL);
93
- }
94
- /**
95
- * Apply maxRows limit to a SELECT query only
96
- *
97
- * This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
98
- * the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
99
- *
100
- * For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
101
- * to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
102
- */
103
- static applyMaxRows(sql, maxRows) {
104
- if (!maxRows || !this.isSelectQuery(sql)) {
105
- return sql;
106
- }
107
- if (this.hasParameterizedLimit(sql)) {
108
- const trimmed = sql.trim();
109
- const hasSemicolon = trimmed.endsWith(";");
110
- const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
111
- return `SELECT * FROM (${sqlWithoutSemicolon}
112
- ) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
113
- }
114
- return this.applyLimitToQuery(sql, maxRows);
115
- }
116
- /**
117
- * Apply maxRows limit to a SELECT query using SQL Server TOP syntax
118
- */
119
- static applyMaxRowsForSQLServer(sql, maxRows) {
120
- if (!maxRows || !this.isSelectQuery(sql)) {
121
- return sql;
122
- }
123
- return this.applyTopToQuery(sql, maxRows);
124
- }
125
- };
126
-
127
- // src/utils/resource-cleanup.ts
128
- async function closeQuietly(close) {
129
- try {
130
- await close();
131
- } catch {
132
- }
133
- }
134
-
135
- export {
136
- SQLRowLimiter,
137
- closeQuietly
138
- };