@bytebase/dbhub 0.22.2 → 0.22.3

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
@@ -99,7 +99,9 @@ var allowedKeywords = {
99
99
  mysql: ["select", "with", "explain", "show", "describe", "desc"],
100
100
  mariadb: ["select", "with", "explain", "show", "describe", "desc"],
101
101
  sqlite: ["select", "with", "explain", "pragma"],
102
- sqlserver: ["select", "with", "explain", "showplan"]
102
+ // SQL Server has no native EXPLAIN statement; the connector translates a
103
+ // leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
104
+ sqlserver: ["select", "with", "explain"]
103
105
  };
104
106
  var mutatingKeywords = [
105
107
  "insert",
@@ -321,6 +323,15 @@ function likePatternToRegex(pattern) {
321
323
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
322
324
  return new RegExp(`^${escaped}$`, "i");
323
325
  }
326
+ async function resolveDefaultSchemas(connector) {
327
+ if (connector.getDefaultSchema) {
328
+ const defaultSchema = await connector.getDefaultSchema();
329
+ if (defaultSchema) {
330
+ return [defaultSchema];
331
+ }
332
+ }
333
+ return connector.getSchemas();
334
+ }
324
335
  async function getTableRowCount(connector, tableName, schemaName) {
325
336
  try {
326
337
  if (connector.getTableRowCount) {
@@ -348,7 +359,7 @@ async function getTableComment(connector, tableName, schemaName) {
348
359
  }
349
360
  }
350
361
  async function searchSchemas(connector, pattern, detailLevel, limit) {
351
- const schemas = await connector.getSchemas();
362
+ const schemas = await resolveDefaultSchemas(connector);
352
363
  const regex = likePatternToRegex(pattern);
353
364
  const matched = schemas.filter((schema) => regex.test(schema)).slice(0, limit);
354
365
  if (detailLevel === "names") {
@@ -379,7 +390,7 @@ async function searchTables(connector, pattern, schemaFilter, detailLevel, limit
379
390
  if (schemaFilter) {
380
391
  schemasToSearch = [schemaFilter];
381
392
  } else {
382
- schemasToSearch = await connector.getSchemas();
393
+ schemasToSearch = await resolveDefaultSchemas(connector);
383
394
  }
384
395
  for (const schemaName of schemasToSearch) {
385
396
  if (results.length >= limit) break;
@@ -532,7 +543,7 @@ async function searchColumns(connector, pattern, schemaFilter, tableFilter, deta
532
543
  if (schemaFilter) {
533
544
  schemasToSearch = [schemaFilter];
534
545
  } else {
535
- schemasToSearch = await connector.getSchemas();
546
+ schemasToSearch = await resolveDefaultSchemas(connector);
536
547
  }
537
548
  for (const schemaName of schemasToSearch) {
538
549
  if (results.length >= limit) break;
@@ -589,7 +600,7 @@ async function searchProcedures(connector, pattern, schemaFilter, detailLevel, l
589
600
  if (schemaFilter) {
590
601
  schemasToSearch = [schemaFilter];
591
602
  } else {
592
- schemasToSearch = await connector.getSchemas();
603
+ schemasToSearch = await resolveDefaultSchemas(connector);
593
604
  }
594
605
  for (const schemaName of schemasToSearch) {
595
606
  if (results.length >= limit) break;
@@ -637,7 +648,7 @@ async function searchIndexes(connector, pattern, schemaFilter, tableFilter, deta
637
648
  if (schemaFilter) {
638
649
  schemasToSearch = [schemaFilter];
639
650
  } else {
640
- schemasToSearch = await connector.getSchemas();
651
+ schemasToSearch = await resolveDefaultSchemas(connector);
641
652
  }
642
653
  for (const schemaName of schemasToSearch) {
643
654
  if (results.length >= limit) break;
@@ -1695,10 +1706,10 @@ See documentation for more details on configuring database connections.
1695
1706
  // src/index.ts
1696
1707
  var connectorModules = [
1697
1708
  { load: () => import("./postgres-EKVOBAH6.js"), name: "PostgreSQL", driver: "pg" },
1698
- { load: () => import("./sqlserver-5E7DSDVX.js"), name: "SQL Server", driver: "mssql" },
1709
+ { load: () => import("./sqlserver-5TFVBCMA.js"), name: "SQL Server", driver: "mssql" },
1699
1710
  { load: () => import("./sqlite-2GBIF3MQ.js"), name: "SQLite", driver: "node:sqlite" },
1700
- { load: () => import("./mysql-NCKXNVZJ.js"), name: "MySQL", driver: "mysql2" },
1701
- { load: () => import("./mariadb-DVQM4MMM.js"), name: "MariaDB", driver: "mariadb" }
1711
+ { load: () => import("./mysql-WG6UFALX.js"), name: "MySQL", driver: "mysql2" },
1712
+ { load: () => import("./mariadb-ERB4D3RP.js"), name: "MariaDB", driver: "mariadb" }
1702
1713
  ];
1703
1714
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1704
1715
  console.error("Fatal error:", error);
@@ -123,8 +123,9 @@ var MariaDBConnector = class _MariaDBConnector {
123
123
  }
124
124
  try {
125
125
  const rows = await this.pool.query(`
126
- SELECT SCHEMA_NAME
126
+ SELECT SCHEMA_NAME
127
127
  FROM INFORMATION_SCHEMA.SCHEMATA
128
+ WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
128
129
  ORDER BY SCHEMA_NAME
129
130
  `);
130
131
  return rows.map((row) => row.SCHEMA_NAME);
@@ -447,6 +448,18 @@ var MariaDBConnector = class _MariaDBConnector {
447
448
  const rows = await this.pool.query("SELECT DATABASE() AS DB");
448
449
  return rows[0].DB;
449
450
  }
451
+ /**
452
+ * Default search scope = the database named in the DSN. DATABASE() returns
453
+ * null when the connection was opened without a database, in which case
454
+ * callers fall back to the full server-wide schema list.
455
+ */
456
+ async getDefaultSchema() {
457
+ if (!this.pool) {
458
+ throw new Error("Not connected to database");
459
+ }
460
+ const rows = await this.pool.query("SELECT DATABASE() AS DB");
461
+ return rows[0]?.DB ?? null;
462
+ }
450
463
  async executeSQL(sql, options, parameters) {
451
464
  if (!this.pool) {
452
465
  throw new Error("Not connected to database");
@@ -129,8 +129,9 @@ var MySQLConnector = class _MySQLConnector {
129
129
  }
130
130
  try {
131
131
  const [rows] = await this.pool.query(`
132
- SELECT SCHEMA_NAME
132
+ SELECT SCHEMA_NAME
133
133
  FROM INFORMATION_SCHEMA.SCHEMATA
134
+ WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
134
135
  ORDER BY SCHEMA_NAME
135
136
  `);
136
137
  return rows.map((row) => row.SCHEMA_NAME);
@@ -453,6 +454,18 @@ var MySQLConnector = class _MySQLConnector {
453
454
  const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
454
455
  return rows[0].DB;
455
456
  }
457
+ /**
458
+ * Default search scope = the database named in the DSN. DATABASE() returns
459
+ * null when the connection was opened without a database, in which case
460
+ * callers fall back to the full server-wide schema list.
461
+ */
462
+ async getDefaultSchema() {
463
+ if (!this.pool) {
464
+ throw new Error("Not connected to database");
465
+ }
466
+ const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
467
+ return rows[0]?.DB ?? null;
468
+ }
456
469
  async executeSQL(sql, options, parameters) {
457
470
  if (!this.pool) {
458
471
  throw new Error("Not connected to database");
@@ -7,7 +7,8 @@ import {
7
7
  import {
8
8
  ConnectorRegistry,
9
9
  SafeURL,
10
- obfuscateDSNPassword
10
+ obfuscateDSNPassword,
11
+ stripCommentsAndStrings
11
12
  } from "./chunk-C7WEAPX4.js";
12
13
 
13
14
  // src/connectors/sqlserver/index.ts
@@ -135,7 +136,7 @@ Expected: ${expectedFormat}`
135
136
  }
136
137
  }
137
138
  };
138
- var SQLServerConnector = class _SQLServerConnector {
139
+ var _SQLServerConnector = class _SQLServerConnector {
139
140
  constructor() {
140
141
  this.id = "sqlserver";
141
142
  this.name = "SQL Server";
@@ -457,6 +458,12 @@ var SQLServerConnector = class _SQLServerConnector {
457
458
  if (!this.connection) {
458
459
  throw new Error("Not connected to SQL Server database");
459
460
  }
461
+ const afterNoise = sqlQuery.slice(
462
+ sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
463
+ );
464
+ if (/^explain\b/i.test(afterNoise)) {
465
+ return this.explainQuery(afterNoise.slice("explain".length).trim());
466
+ }
460
467
  try {
461
468
  let processedSQL = sqlQuery;
462
469
  if (options.maxRows) {
@@ -518,7 +525,60 @@ var SQLServerConnector = class _SQLServerConnector {
518
525
  throw new Error(`Failed to execute query: ${error.message}`);
519
526
  }
520
527
  }
528
+ /**
529
+ * Return the estimated execution plan for a query using SHOWPLAN_XML.
530
+ *
531
+ * SHOWPLAN_XML compiles the statement and returns its plan without executing
532
+ * it, but it has two constraints: `SET SHOWPLAN_XML ON` must be the only
533
+ * statement in its batch, and the setting is session scoped. The shared pool
534
+ * hands out a fresh connection per request() and an open transaction
535
+ * suppresses SHOWPLAN, so neither can carry the setting to a follow-up query.
536
+ *
537
+ * We therefore run the SET / query pair on a short-lived, single-connection
538
+ * pool built from the same config. The dedicated session keeps SHOWPLAN state
539
+ * off the shared pool, so a concurrent query can never land on a connection
540
+ * with SHOWPLAN enabled (which would return a plan instead of its results).
541
+ */
542
+ async explainQuery(innerQuery) {
543
+ const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
544
+ if (!cleaned) {
545
+ throw new Error("EXPLAIN requires a statement to analyze");
546
+ }
547
+ if (/\bset\s+showplan/i.test(cleaned)) {
548
+ throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
549
+ }
550
+ if (!this.config) {
551
+ throw new Error("Not connected to SQL Server database");
552
+ }
553
+ const explainPool = new sql.ConnectionPool({
554
+ ...this.config,
555
+ pool: { ...this.config.pool, max: 1, min: 1 }
556
+ });
557
+ try {
558
+ await explainPool.connect();
559
+ await explainPool.request().batch("SET SHOWPLAN_XML ON");
560
+ const planResult = await explainPool.request().batch(innerQuery);
561
+ const planRow = planResult.recordset?.[0];
562
+ const planXml = planRow ? Object.values(planRow)[0] : null;
563
+ return {
564
+ rows: planXml != null ? [{ plan: planXml }] : [],
565
+ rowCount: planXml != null ? 1 : 0
566
+ };
567
+ } catch (error) {
568
+ throw new Error(`Failed to explain query: ${error.message}`);
569
+ } finally {
570
+ await explainPool.close();
571
+ }
572
+ }
521
573
  };
574
+ /**
575
+ * Leading whitespace and SQL comments to skip before looking for a keyword.
576
+ * The read-only validator strips comments before checking the first keyword,
577
+ * so the connector must skip them too; otherwise an EXPLAIN preceded by a
578
+ * comment passes validation but reaches the server untranslated.
579
+ */
580
+ _SQLServerConnector.LEADING_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
581
+ var SQLServerConnector = _SQLServerConnector;
522
582
  var sqlServerConnector = new SQLServerConnector();
523
583
  ConnectorRegistry.register(sqlServerConnector);
524
584
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.22.2",
3
+ "version": "0.22.3",
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": {