@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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
1701
|
-
{ load: () => import("./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
|
|
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