@bytebase/dbhub 0.23.0 → 1.0.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.
- package/README.md +6 -0
- package/dist/{chunk-RTB262PR.js → chunk-DUCNPMNW.js} +54 -1
- package/dist/chunk-JO7YQPBQ.js +130 -0
- package/dist/{chunk-COEDRFSK.js → chunk-MQR6VX5F.js} +54 -40
- package/dist/{chunk-BXXHWEQS.js → chunk-S2V6RZ3C.js} +14 -3
- package/dist/index.js +104 -154
- package/dist/{mariadb-SPFBL46R.js → mariadb-72C2BNCG.js} +51 -48
- package/dist/{mysql-MZWCPISB.js → mysql-CDUKJMNN.js} +52 -49
- package/dist/{postgres-BKBCGDZS.js → postgres-YAYHDXVD.js} +9 -15
- package/dist/{registry-ZME64VB2.js → registry-ZU6ZFNQ2.js} +1 -1
- package/dist/{sqlite-CQ4LCBB6.js → sqlite-5M7SDAQZ.js} +11 -19
- package/dist/{sqlserver-65AGCEJX.js → sqlserver-COKW7RIK.js} +285 -81
- package/package.json +5 -3
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sqlServerDynamicSqlKeywords,
|
|
3
|
+
sqlServerDynamicSqlPattern,
|
|
4
|
+
sqlServerPassThroughKeywords,
|
|
5
|
+
sqlServerPassThroughPattern
|
|
6
|
+
} from "./chunk-JO7YQPBQ.js";
|
|
1
7
|
import {
|
|
2
8
|
isDriverNotInstalled
|
|
3
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
4
10
|
import {
|
|
5
|
-
SQLRowLimiter
|
|
6
|
-
|
|
11
|
+
SQLRowLimiter,
|
|
12
|
+
closeQuietly
|
|
13
|
+
} from "./chunk-S2V6RZ3C.js";
|
|
7
14
|
import {
|
|
8
15
|
ConnectorRegistry,
|
|
9
16
|
SafeURL,
|
|
@@ -156,8 +163,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
156
163
|
if (!this.config.options) {
|
|
157
164
|
this.config.options = {};
|
|
158
165
|
}
|
|
159
|
-
this.connection =
|
|
166
|
+
this.connection = new sql.ConnectionPool(this.config);
|
|
167
|
+
await this.connection.connect();
|
|
160
168
|
} catch (error) {
|
|
169
|
+
if (this.connection) {
|
|
170
|
+
const connection = this.connection;
|
|
171
|
+
this.connection = void 0;
|
|
172
|
+
await closeQuietly(() => connection.close());
|
|
173
|
+
}
|
|
161
174
|
throw error;
|
|
162
175
|
}
|
|
163
176
|
}
|
|
@@ -462,7 +475,10 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
462
475
|
sqlQuery.match(_SQLServerConnector.LEADING_NOISE)[0].length
|
|
463
476
|
);
|
|
464
477
|
if (/^explain\b/i.test(afterNoise)) {
|
|
465
|
-
|
|
478
|
+
const { analyze, query } = _SQLServerConnector.parseExplainPrefix(
|
|
479
|
+
afterNoise.slice("explain".length).trim()
|
|
480
|
+
);
|
|
481
|
+
return analyze ? this.explainAnalyzeQuery(query, options, parameters) : this.explainQuery(query, options.readonly, parameters);
|
|
466
482
|
}
|
|
467
483
|
try {
|
|
468
484
|
let processedSQL = sqlQuery;
|
|
@@ -486,39 +502,8 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
486
502
|
});
|
|
487
503
|
}
|
|
488
504
|
);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const paramName = `p${index + 1}`;
|
|
492
|
-
if (typeof param === "string") {
|
|
493
|
-
request.input(paramName, sql.VarChar, param);
|
|
494
|
-
} else if (typeof param === "number") {
|
|
495
|
-
if (Number.isInteger(param)) {
|
|
496
|
-
request.input(paramName, sql.Int, param);
|
|
497
|
-
} else {
|
|
498
|
-
request.input(paramName, sql.Float, param);
|
|
499
|
-
}
|
|
500
|
-
} else if (typeof param === "boolean") {
|
|
501
|
-
request.input(paramName, sql.Bit, param);
|
|
502
|
-
} else if (param === null || param === void 0) {
|
|
503
|
-
request.input(paramName, sql.VarChar, param);
|
|
504
|
-
} else if (Array.isArray(param)) {
|
|
505
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
506
|
-
} else {
|
|
507
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
508
|
-
}
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
let result;
|
|
512
|
-
try {
|
|
513
|
-
result = await request.query(processedSQL);
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (parameters && parameters.length > 0) {
|
|
516
|
-
console.error(`[SQL Server executeSQL] ERROR: ${error.message}`);
|
|
517
|
-
console.error(`[SQL Server executeSQL] SQL: ${processedSQL}`);
|
|
518
|
-
console.error(`[SQL Server executeSQL] Parameters: ${JSON.stringify(parameters)}`);
|
|
519
|
-
}
|
|
520
|
-
throw error;
|
|
521
|
-
}
|
|
505
|
+
_SQLServerConnector.bindParameters(request, parameters);
|
|
506
|
+
const result = await request.query(processedSQL);
|
|
522
507
|
return {
|
|
523
508
|
rows: result.recordset || [],
|
|
524
509
|
rowCount: result.rowsAffected[0] || 0,
|
|
@@ -529,30 +514,88 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
529
514
|
}
|
|
530
515
|
}
|
|
531
516
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* keyword classifier.
|
|
517
|
+
* Bind positional parameters as @p1, @p2, ... inferring the SQL Server type
|
|
518
|
+
* from each JavaScript value. Shared by every execution path so they cannot
|
|
519
|
+
* drift in how a given value is typed.
|
|
536
520
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
* transaction:
|
|
540
|
-
* - COMMIT/ROLLBACK: would end the outer transaction, letting writes persist
|
|
541
|
-
* - EXEC/EXECUTE/sp_executesql/xp_cmdshell: dynamic SQL can carry hidden
|
|
542
|
-
* COMMIT/ROLLBACK inside string literals that stripCommentsAndStrings removes
|
|
521
|
+
* Works for `batch` as well as `query`: node-mssql prepends the matching
|
|
522
|
+
* DECLARE/SET statements when the request is a batch.
|
|
543
523
|
*/
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
524
|
+
static bindParameters(request, parameters) {
|
|
525
|
+
if (!parameters || parameters.length === 0) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
parameters.forEach((param, index) => {
|
|
529
|
+
const paramName = `p${index + 1}`;
|
|
530
|
+
if (typeof param === "string") {
|
|
531
|
+
request.input(paramName, sql.VarChar, param);
|
|
532
|
+
} else if (typeof param === "number") {
|
|
533
|
+
if (Number.isInteger(param)) {
|
|
534
|
+
request.input(paramName, sql.Int, param);
|
|
535
|
+
} else {
|
|
536
|
+
request.input(paramName, sql.Float, param);
|
|
537
|
+
}
|
|
538
|
+
} else if (typeof param === "boolean") {
|
|
539
|
+
request.input(paramName, sql.Bit, param);
|
|
540
|
+
} else if (param === null || param === void 0) {
|
|
541
|
+
request.input(paramName, sql.VarChar, param);
|
|
542
|
+
} else if (Array.isArray(param)) {
|
|
543
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
544
|
+
} else {
|
|
545
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Reject the constructs that escape SQL Server's read-only guards, for use by
|
|
551
|
+
* both read-only execution paths.
|
|
552
|
+
*
|
|
553
|
+
* - Dynamic SQL (sqlServerDynamicSqlKeywords): can carry hidden COMMIT/ROLLBACK
|
|
554
|
+
* inside string literals that stripCommentsAndStrings removes
|
|
555
|
+
* - Pass-through data sources (sqlServerPassThroughKeywords): execute on a
|
|
556
|
+
* remote or ad-hoc source, so a local rollback never reaches them
|
|
557
|
+
* - COMMIT/ROLLBACK, when `transactionControl` is set: would end the wrapping
|
|
558
|
+
* transaction, letting writes persist. Only meaningful for the transaction
|
|
559
|
+
* path; the EXPLAIN path opens no transaction of its own.
|
|
560
|
+
*
|
|
561
|
+
* Both keyword lists are imported from the read-only classifier rather than
|
|
562
|
+
* redeclared, so the classifier and these backstops cannot drift apart.
|
|
563
|
+
*
|
|
564
|
+
* Note the COMMIT/ROLLBACK check is SQL Server-only by design. MySQL/MariaDB
|
|
565
|
+
* wrap batches in a transaction too, but there `commit`, `prepare` and
|
|
566
|
+
* `execute` are absent from their allow-lists in allowedKeywords, and
|
|
567
|
+
* execute-sql.ts requires every split statement to pass the classifier — so a
|
|
568
|
+
* transaction-control statement can never reach their backstop.
|
|
569
|
+
*/
|
|
570
|
+
assertNoReadOnlyEscapes(sqlText, { transactionControl = false } = {}) {
|
|
571
|
+
const cleaned = stripCommentsAndStrings(sqlText, "sqlserver").toLowerCase();
|
|
572
|
+
if (transactionControl && /\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
547
573
|
throw new Error(
|
|
548
574
|
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
549
575
|
);
|
|
550
576
|
}
|
|
551
|
-
if (
|
|
577
|
+
if (sqlServerDynamicSqlPattern.test(cleaned)) {
|
|
552
578
|
throw new Error(
|
|
553
|
-
|
|
579
|
+
`Read-only mode: dynamic SQL execution (${sqlServerDynamicSqlKeywords.map((k) => k.toUpperCase()).join(", ")}) is not allowed`
|
|
554
580
|
);
|
|
555
581
|
}
|
|
582
|
+
if (sqlServerPassThroughPattern.test(cleaned)) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Read-only mode: pass-through data sources (${sqlServerPassThroughKeywords.map((k) => k.toUpperCase()).join(", ")}) are not allowed`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Execute a query inside a transaction that always rolls back, preventing
|
|
590
|
+
* any modifications from persisting. SQL Server has no native READ ONLY
|
|
591
|
+
* transaction mode, so this is the defense-in-depth backstop behind the
|
|
592
|
+
* keyword classifier.
|
|
593
|
+
*
|
|
594
|
+
* Dangerous constructs are rejected before the transaction opens; see
|
|
595
|
+
* assertNoReadOnlyEscapes.
|
|
596
|
+
*/
|
|
597
|
+
async executeReadOnly(processedSQL, parameters) {
|
|
598
|
+
this.assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
556
599
|
const transaction = new sql.Transaction(this.connection);
|
|
557
600
|
await transaction.begin();
|
|
558
601
|
const request = new sql.Request(transaction);
|
|
@@ -568,39 +611,13 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
568
611
|
});
|
|
569
612
|
}
|
|
570
613
|
);
|
|
571
|
-
|
|
572
|
-
parameters.forEach((param, index) => {
|
|
573
|
-
const paramName = `p${index + 1}`;
|
|
574
|
-
if (typeof param === "string") {
|
|
575
|
-
request.input(paramName, sql.VarChar, param);
|
|
576
|
-
} else if (typeof param === "number") {
|
|
577
|
-
if (Number.isInteger(param)) {
|
|
578
|
-
request.input(paramName, sql.Int, param);
|
|
579
|
-
} else {
|
|
580
|
-
request.input(paramName, sql.Float, param);
|
|
581
|
-
}
|
|
582
|
-
} else if (typeof param === "boolean") {
|
|
583
|
-
request.input(paramName, sql.Bit, param);
|
|
584
|
-
} else if (param === null || param === void 0) {
|
|
585
|
-
request.input(paramName, sql.VarChar, param);
|
|
586
|
-
} else if (Array.isArray(param)) {
|
|
587
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
588
|
-
} else {
|
|
589
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
}
|
|
614
|
+
_SQLServerConnector.bindParameters(request, parameters);
|
|
593
615
|
let result;
|
|
594
616
|
let queryFailed = false;
|
|
595
617
|
try {
|
|
596
618
|
result = await request.query(processedSQL);
|
|
597
619
|
} catch (error) {
|
|
598
620
|
queryFailed = true;
|
|
599
|
-
console.error(`[SQL Server executeReadOnly] ERROR: ${error.message}`);
|
|
600
|
-
console.error(`[SQL Server executeReadOnly] SQL: ${processedSQL}`);
|
|
601
|
-
if (parameters && parameters.length > 0) {
|
|
602
|
-
console.error(`[SQL Server executeReadOnly] Parameters: ${JSON.stringify(parameters)}`);
|
|
603
|
-
}
|
|
604
621
|
throw error;
|
|
605
622
|
} finally {
|
|
606
623
|
try {
|
|
@@ -633,11 +650,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
633
650
|
* off the shared pool, so a concurrent query can never land on a connection
|
|
634
651
|
* with SHOWPLAN enabled (which would return a plan instead of its results).
|
|
635
652
|
*/
|
|
636
|
-
async explainQuery(innerQuery) {
|
|
653
|
+
async explainQuery(innerQuery, readonly, parameters) {
|
|
637
654
|
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
638
655
|
if (!cleaned) {
|
|
639
656
|
throw new Error("EXPLAIN requires a statement to analyze");
|
|
640
657
|
}
|
|
658
|
+
if (readonly) {
|
|
659
|
+
this.assertNoReadOnlyEscapes(innerQuery);
|
|
660
|
+
}
|
|
641
661
|
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
642
662
|
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
643
663
|
}
|
|
@@ -651,7 +671,9 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
651
671
|
try {
|
|
652
672
|
await explainPool.connect();
|
|
653
673
|
await explainPool.request().batch("SET SHOWPLAN_XML ON");
|
|
654
|
-
const
|
|
674
|
+
const planRequest = explainPool.request();
|
|
675
|
+
_SQLServerConnector.bindParameters(planRequest, parameters);
|
|
676
|
+
const planResult = await planRequest.batch(innerQuery);
|
|
655
677
|
const planRow = planResult.recordset?.[0];
|
|
656
678
|
const planXml = planRow ? Object.values(planRow)[0] : null;
|
|
657
679
|
return {
|
|
@@ -664,6 +686,177 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
664
686
|
await explainPool.close();
|
|
665
687
|
}
|
|
666
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Splits the modifiers between EXPLAIN and its statement, covering the same
|
|
691
|
+
* forms the read-only classifier recognises (see utils/allowed-keywords.ts):
|
|
692
|
+
* `ANALYZE`, `ANALYZE VERBOSE`, `(ANALYZE)`, `(ANALYZE, BUFFERS)`,
|
|
693
|
+
* `(ANALYZE false)`.
|
|
694
|
+
*
|
|
695
|
+
* Only ANALYZE carries a meaning here — it selects STATISTICS XML over
|
|
696
|
+
* SHOWPLAN_XML. The rest are PostgreSQL planner knobs with no SQL Server
|
|
697
|
+
* counterpart, so they are refused by name: silently dropping an option would
|
|
698
|
+
* quietly hand back something other than what was asked for.
|
|
699
|
+
*/
|
|
700
|
+
static parseExplainPrefix(afterExplain) {
|
|
701
|
+
if (afterExplain.startsWith("(")) {
|
|
702
|
+
const close = afterExplain.indexOf(")");
|
|
703
|
+
if (close < 0) {
|
|
704
|
+
throw new Error("EXPLAIN option list is missing its closing ')'");
|
|
705
|
+
}
|
|
706
|
+
let analyze = false;
|
|
707
|
+
for (const part of afterExplain.slice(1, close).split(",")) {
|
|
708
|
+
const token = part.trim();
|
|
709
|
+
if (!token) continue;
|
|
710
|
+
const parsed = _SQLServerConnector.EXPLAIN_OPTION.exec(token);
|
|
711
|
+
if (!parsed) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`EXPLAIN option '${token}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const name = parsed[1];
|
|
717
|
+
if (!/^analyze$/i.test(name)) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
`EXPLAIN option '${name}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const value = parsed[2];
|
|
723
|
+
if (value === void 0 || _SQLServerConnector.EXPLAIN_ON.test(value)) {
|
|
724
|
+
analyze = true;
|
|
725
|
+
} else if (_SQLServerConnector.EXPLAIN_OFF.test(value)) {
|
|
726
|
+
analyze = false;
|
|
727
|
+
} else {
|
|
728
|
+
throw new Error(`EXPLAIN option 'ANALYZE' expects a boolean, got '${value}'.`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return { analyze, query: afterExplain.slice(close + 1).trim() };
|
|
732
|
+
}
|
|
733
|
+
const analyzeKeyword = /^analyze\b/i.exec(afterExplain);
|
|
734
|
+
if (!analyzeKeyword) {
|
|
735
|
+
return { analyze: false, query: afterExplain };
|
|
736
|
+
}
|
|
737
|
+
const rest = afterExplain.slice(analyzeKeyword[0].length).trim();
|
|
738
|
+
const disabled = _SQLServerConnector.EXPLAIN_BARE_DISABLED.exec(rest);
|
|
739
|
+
if (disabled) {
|
|
740
|
+
return { analyze: false, query: rest.slice(disabled[0].length).trim() };
|
|
741
|
+
}
|
|
742
|
+
const trailing = /^([A-Za-z_]+)\b/.exec(rest);
|
|
743
|
+
if (trailing && /^verbose$/i.test(trailing[1])) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
"EXPLAIN option 'VERBOSE' is not supported on SQL Server \u2014 only ANALYZE is."
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return { analyze: true, query: rest };
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Run a statement under SET STATISTICS XML and return its *actual* execution
|
|
752
|
+
* plan — the Postgres `EXPLAIN ANALYZE` contract.
|
|
753
|
+
*
|
|
754
|
+
* SHOWPLAN_XML (plain EXPLAIN) compiles without executing, so its plan carries
|
|
755
|
+
* only estimates. STATISTICS XML runs the statement, so the plan reports real
|
|
756
|
+
* row counts and execution counts. The flip side is that this path is *not*
|
|
757
|
+
* inherently read-only the way explainQuery is, so under `options.readonly` the
|
|
758
|
+
* statement runs inside a transaction that always rolls back.
|
|
759
|
+
*
|
|
760
|
+
* The dedicated single-connection pool serves the same purpose as in
|
|
761
|
+
* explainQuery: the STATISTICS XML session toggle must never leak onto a
|
|
762
|
+
* shared pool connection, where a concurrent query would inherit it.
|
|
763
|
+
*/
|
|
764
|
+
async explainAnalyzeQuery(innerQuery, options, parameters) {
|
|
765
|
+
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
766
|
+
if (!cleaned) {
|
|
767
|
+
throw new Error("EXPLAIN ANALYZE requires a statement to analyze");
|
|
768
|
+
}
|
|
769
|
+
if (/\bset\s+statistics\b/i.test(cleaned)) {
|
|
770
|
+
throw new Error("EXPLAIN ANALYZE does not support SET STATISTICS statements");
|
|
771
|
+
}
|
|
772
|
+
if (/\bset\s+showplan\b/i.test(cleaned)) {
|
|
773
|
+
throw new Error("EXPLAIN ANALYZE does not support SET SHOWPLAN statements");
|
|
774
|
+
}
|
|
775
|
+
if (options.readonly) {
|
|
776
|
+
this.assertNoReadOnlyEscapes(innerQuery, { transactionControl: true });
|
|
777
|
+
}
|
|
778
|
+
if (!this.config) {
|
|
779
|
+
throw new Error("Not connected to SQL Server database");
|
|
780
|
+
}
|
|
781
|
+
const explainPool = new sql.ConnectionPool({
|
|
782
|
+
...this.config,
|
|
783
|
+
pool: { ...this.config.pool, max: 1, min: 1 }
|
|
784
|
+
});
|
|
785
|
+
try {
|
|
786
|
+
await explainPool.connect();
|
|
787
|
+
await explainPool.request().batch("SET STATISTICS XML ON");
|
|
788
|
+
let planResult;
|
|
789
|
+
if (options.readonly) {
|
|
790
|
+
planResult = await _SQLServerConnector.batchRolledBack(
|
|
791
|
+
explainPool,
|
|
792
|
+
innerQuery,
|
|
793
|
+
parameters
|
|
794
|
+
);
|
|
795
|
+
} else {
|
|
796
|
+
const planRequest = explainPool.request();
|
|
797
|
+
_SQLServerConnector.bindParameters(planRequest, parameters);
|
|
798
|
+
planResult = await planRequest.batch(innerQuery);
|
|
799
|
+
}
|
|
800
|
+
const planXml = _SQLServerConnector.extractPlanXml(planResult);
|
|
801
|
+
return {
|
|
802
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
803
|
+
rowCount: planXml != null ? 1 : 0
|
|
804
|
+
};
|
|
805
|
+
} catch (error) {
|
|
806
|
+
throw new Error(`Failed to explain analyze query: ${error.message}`);
|
|
807
|
+
} finally {
|
|
808
|
+
await explainPool.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Run a batch inside a transaction that is always rolled back, so EXPLAIN
|
|
813
|
+
* ANALYZE can report a real plan without letting the statement's writes stick.
|
|
814
|
+
*/
|
|
815
|
+
static async batchRolledBack(pool, innerQuery, parameters) {
|
|
816
|
+
const transaction = new sql.Transaction(pool);
|
|
817
|
+
await transaction.begin();
|
|
818
|
+
let queryFailed = false;
|
|
819
|
+
try {
|
|
820
|
+
const request = new sql.Request(transaction);
|
|
821
|
+
_SQLServerConnector.bindParameters(request, parameters);
|
|
822
|
+
return await request.batch(innerQuery);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
queryFailed = true;
|
|
825
|
+
throw error;
|
|
826
|
+
} finally {
|
|
827
|
+
try {
|
|
828
|
+
await transaction.rollback();
|
|
829
|
+
} catch (rollbackError) {
|
|
830
|
+
if (!queryFailed) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`Read-only rollback failed \u2014 data may have been modified: ${rollbackError.message}`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Pull the ShowPlanXML document out of a STATISTICS XML result.
|
|
840
|
+
*
|
|
841
|
+
* STATISTICS XML interleaves each statement's plan with that statement's own
|
|
842
|
+
* result sets, so the plan sits at no fixed index — and `recordset` (singular)
|
|
843
|
+
* would hand back the statement's data instead. Scan from the end for the
|
|
844
|
+
* first single-column row holding a plan document.
|
|
845
|
+
*/
|
|
846
|
+
static extractPlanXml(result) {
|
|
847
|
+
const recordsets = result.recordsets ?? [];
|
|
848
|
+
for (let i = recordsets.length - 1; i >= 0; i--) {
|
|
849
|
+
const firstRow = recordsets[i]?.[0];
|
|
850
|
+
if (!firstRow) continue;
|
|
851
|
+
const values = Object.values(firstRow);
|
|
852
|
+
if (values.length !== 1) continue;
|
|
853
|
+
const value = values[0];
|
|
854
|
+
if (typeof value === "string" && value.includes("<ShowPlanXML")) {
|
|
855
|
+
return value;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
667
860
|
};
|
|
668
861
|
/**
|
|
669
862
|
* Leading whitespace and SQL comments to skip before looking for a keyword.
|
|
@@ -672,6 +865,17 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
672
865
|
* comment passes validation but reaches the server untranslated.
|
|
673
866
|
*/
|
|
674
867
|
_SQLServerConnector.LEADING_NOISE = /^(?:\s+|--[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/;
|
|
868
|
+
/** Boolean spellings PostgreSQL accepts for an EXPLAIN option. */
|
|
869
|
+
_SQLServerConnector.EXPLAIN_ON = /^(?:true|on|1)$/i;
|
|
870
|
+
_SQLServerConnector.EXPLAIN_OFF = /^(?:false|off|0)$/i;
|
|
871
|
+
/**
|
|
872
|
+
* One option inside `EXPLAIN (...)`: a name, then optionally a value written
|
|
873
|
+
* either space-separated as PostgreSQL spells it (`ANALYZE false`) or with an
|
|
874
|
+
* equals sign, which the read-only classifier also accepts (`ANALYZE = 0`).
|
|
875
|
+
*/
|
|
876
|
+
_SQLServerConnector.EXPLAIN_OPTION = /^([A-Za-z_]+)(?:(?:\s*=\s*|\s+)(\S+))?$/;
|
|
877
|
+
/** A disabling boolean directly after a bare `EXPLAIN ANALYZE`. */
|
|
878
|
+
_SQLServerConnector.EXPLAIN_BARE_DISABLED = /^(?:=\s*)?(?:false|off|0)\b/i;
|
|
675
879
|
var SQLServerConnector = _SQLServerConnector;
|
|
676
880
|
var sqlServerConnector = new SQLServerConnector();
|
|
677
881
|
ConnectorRegistry.register(sqlServerConnector);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebase/dbhub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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": {
|
|
@@ -44,12 +44,13 @@
|
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@iarna/toml": "^2.2.5",
|
|
47
|
-
"@modelcontextprotocol/
|
|
47
|
+
"@modelcontextprotocol/node": "^2.0.0",
|
|
48
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
48
49
|
"dotenv": "^16.4.7",
|
|
49
50
|
"express": "^4.18.2",
|
|
50
51
|
"ssh-config": "^5.0.3",
|
|
51
52
|
"ssh2": "^1.16.0",
|
|
52
|
-
"zod": "^
|
|
53
|
+
"zod": "^4.2.0"
|
|
53
54
|
},
|
|
54
55
|
"optionalDependencies": {
|
|
55
56
|
"@aws-sdk/rds-signer": "^3.1001.0",
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@anthropic-ai/mcpb": "^2.1.2",
|
|
65
|
+
"@modelcontextprotocol/client": "^2.0.0",
|
|
64
66
|
"@testcontainers/mariadb": "^11.0.3",
|
|
65
67
|
"@testcontainers/mssqlserver": "^11.0.3",
|
|
66
68
|
"@testcontainers/mysql": "^11.0.3",
|