@bytebase/dbhub 1.3.1 → 1.5.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/dist/{chunk-ABTVMHD6.js → chunk-55X2VY5D.js} +1 -1
- package/dist/chunk-5XSBOK3W.js +63 -0
- package/dist/{chunk-5FL4XMWG.js → chunk-PDKPK25P.js} +356 -106
- package/dist/chunk-SJ5BRPQA.js +500 -0
- package/dist/{chunk-YIGJHEU6.js → chunk-X5OCFRNL.js} +170 -27
- package/dist/index.js +116 -32
- package/dist/mariadb-RWOMZTA6.js +270 -0
- package/dist/mysql-ZXO4TWLG.js +312 -0
- package/dist/{oracle-OUKBU52X.js → oracle-UAVN3F4U.js} +2 -2
- package/dist/{postgres-6XJ7V4J4.js → postgres-KCFNDQLG.js} +179 -29
- package/dist/{registry-ZTHDPLKP.js → registry-ZIVIYCB2.js} +2 -1
- package/dist/{sqlite-AO2BKIC6.js → sqlite-WKY6543Q.js} +2 -2
- package/dist/{sqlserver-AJUK6L55.js → sqlserver-25MAE77Q.js} +312 -372
- package/package.json +1 -1
- package/dist/chunk-6SZKW6GD.js +0 -179
- package/dist/mariadb-L7Z55ROY.js +0 -551
- package/dist/mysql-XXIHWQGR.js +0 -601
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
SQLRowLimiter,
|
|
10
10
|
closeQuietly
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-X5OCFRNL.js";
|
|
12
12
|
import {
|
|
13
13
|
sqlServerDynamicSqlKeywords,
|
|
14
14
|
sqlServerDynamicSqlPattern,
|
|
15
15
|
sqlServerPassThroughKeywords,
|
|
16
16
|
sqlServerPassThroughPattern
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-55X2VY5D.js";
|
|
18
18
|
import {
|
|
19
19
|
ConnectorRegistry,
|
|
20
20
|
LEADING_SQL_NOISE,
|
|
@@ -25,7 +25,234 @@ import {
|
|
|
25
25
|
} from "./chunk-IXC4NQGC.js";
|
|
26
26
|
|
|
27
27
|
// src/connectors/sqlserver/index.ts
|
|
28
|
+
import sql3 from "mssql";
|
|
29
|
+
|
|
30
|
+
// src/connectors/sqlserver/request-helpers.ts
|
|
28
31
|
import sql from "mssql";
|
|
32
|
+
function bindParameters(request, parameters) {
|
|
33
|
+
if (!parameters || parameters.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
parameters.forEach((param, index) => {
|
|
37
|
+
const paramName = `p${index + 1}`;
|
|
38
|
+
if (typeof param === "string") {
|
|
39
|
+
request.input(paramName, sql.VarChar, param);
|
|
40
|
+
} else if (typeof param === "number") {
|
|
41
|
+
if (Number.isInteger(param)) {
|
|
42
|
+
request.input(paramName, sql.Int, param);
|
|
43
|
+
} else {
|
|
44
|
+
request.input(paramName, sql.Float, param);
|
|
45
|
+
}
|
|
46
|
+
} else if (typeof param === "boolean") {
|
|
47
|
+
request.input(paramName, sql.Bit, param);
|
|
48
|
+
} else if (param === null || param === void 0) {
|
|
49
|
+
request.input(paramName, sql.VarChar, param);
|
|
50
|
+
} else if (Array.isArray(param)) {
|
|
51
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
52
|
+
} else {
|
|
53
|
+
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function assertNoReadOnlyEscapes(sqlText, { transactionControl = false } = {}) {
|
|
58
|
+
const cleaned = stripCommentsAndStrings(sqlText, "sqlserver").toLowerCase();
|
|
59
|
+
if (transactionControl && /\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (sqlServerDynamicSqlPattern.test(cleaned)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Read-only mode: dynamic SQL execution (${sqlServerDynamicSqlKeywords.map((k) => k.toUpperCase()).join(", ")}) is not allowed`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (sqlServerPassThroughPattern.test(cleaned)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Read-only mode: pass-through data sources (${sqlServerPassThroughKeywords.map((k) => k.toUpperCase()).join(", ")}) are not allowed`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/connectors/sqlserver/explain.ts
|
|
77
|
+
import sql2 from "mssql";
|
|
78
|
+
var EXPLAIN_ON = /^(?:true|on|1)$/i;
|
|
79
|
+
var EXPLAIN_OFF = /^(?:false|off|0)$/i;
|
|
80
|
+
var EXPLAIN_OPTION = /^([A-Za-z_]+)(?:(?:\s*=\s*|\s+)(\S+))?$/;
|
|
81
|
+
var EXPLAIN_BARE_DISABLED = /^(?:=\s*)?(?:false|off|0)\b/i;
|
|
82
|
+
async function explainQuery(config, innerQuery, readonly, parameters) {
|
|
83
|
+
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
84
|
+
if (!cleaned) {
|
|
85
|
+
throw new Error("EXPLAIN requires a statement to analyze");
|
|
86
|
+
}
|
|
87
|
+
if (readonly) {
|
|
88
|
+
assertNoReadOnlyEscapes(innerQuery);
|
|
89
|
+
}
|
|
90
|
+
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
91
|
+
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
92
|
+
}
|
|
93
|
+
const explainPool = new sql2.ConnectionPool({
|
|
94
|
+
...config,
|
|
95
|
+
pool: { ...config.pool, max: 1, min: 1 }
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
await explainPool.connect();
|
|
99
|
+
await explainPool.request().batch("SET SHOWPLAN_XML ON");
|
|
100
|
+
const planRequest = explainPool.request();
|
|
101
|
+
bindParameters(planRequest, parameters);
|
|
102
|
+
const planResult = await planRequest.batch(innerQuery);
|
|
103
|
+
const planRow = planResult.recordset?.[0];
|
|
104
|
+
const planXml = planRow ? Object.values(planRow)[0] : null;
|
|
105
|
+
return {
|
|
106
|
+
resultSets: [
|
|
107
|
+
{
|
|
108
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
109
|
+
rowCount: planXml != null ? 1 : 0
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new Error(`Failed to explain query: ${error.message}`);
|
|
115
|
+
} finally {
|
|
116
|
+
await explainPool.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseExplainPrefix(afterExplain) {
|
|
120
|
+
if (afterExplain.startsWith("(")) {
|
|
121
|
+
const close = afterExplain.indexOf(")");
|
|
122
|
+
if (close < 0) {
|
|
123
|
+
throw new Error("EXPLAIN option list is missing its closing ')'");
|
|
124
|
+
}
|
|
125
|
+
let analyze = false;
|
|
126
|
+
for (const part of afterExplain.slice(1, close).split(",")) {
|
|
127
|
+
const token = part.trim();
|
|
128
|
+
if (!token) continue;
|
|
129
|
+
const parsed = EXPLAIN_OPTION.exec(token);
|
|
130
|
+
if (!parsed) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`EXPLAIN option '${token}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const name = parsed[1];
|
|
136
|
+
if (!/^analyze$/i.test(name)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`EXPLAIN option '${name}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const value = parsed[2];
|
|
142
|
+
if (value === void 0 || EXPLAIN_ON.test(value)) {
|
|
143
|
+
analyze = true;
|
|
144
|
+
} else if (EXPLAIN_OFF.test(value)) {
|
|
145
|
+
analyze = false;
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error(`EXPLAIN option 'ANALYZE' expects a boolean, got '${value}'.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { analyze, query: afterExplain.slice(close + 1).trim() };
|
|
151
|
+
}
|
|
152
|
+
const analyzeKeyword = /^analyze\b/i.exec(afterExplain);
|
|
153
|
+
if (!analyzeKeyword) {
|
|
154
|
+
return { analyze: false, query: afterExplain };
|
|
155
|
+
}
|
|
156
|
+
const rest = afterExplain.slice(analyzeKeyword[0].length).trim();
|
|
157
|
+
const disabled = EXPLAIN_BARE_DISABLED.exec(rest);
|
|
158
|
+
if (disabled) {
|
|
159
|
+
return { analyze: false, query: rest.slice(disabled[0].length).trim() };
|
|
160
|
+
}
|
|
161
|
+
const trailing = /^([A-Za-z_]+)\b/.exec(rest);
|
|
162
|
+
if (trailing && /^verbose$/i.test(trailing[1])) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"EXPLAIN option 'VERBOSE' is not supported on SQL Server \u2014 only ANALYZE is."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return { analyze: true, query: rest };
|
|
168
|
+
}
|
|
169
|
+
async function explainAnalyzeQuery(config, innerQuery, options, parameters) {
|
|
170
|
+
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
171
|
+
if (!cleaned) {
|
|
172
|
+
throw new Error("EXPLAIN ANALYZE requires a statement to analyze");
|
|
173
|
+
}
|
|
174
|
+
if (/\bset\s+statistics\b/i.test(cleaned)) {
|
|
175
|
+
throw new Error("EXPLAIN ANALYZE does not support SET STATISTICS statements");
|
|
176
|
+
}
|
|
177
|
+
if (/\bset\s+showplan\b/i.test(cleaned)) {
|
|
178
|
+
throw new Error("EXPLAIN ANALYZE does not support SET SHOWPLAN statements");
|
|
179
|
+
}
|
|
180
|
+
if (options.readonly) {
|
|
181
|
+
assertNoReadOnlyEscapes(innerQuery, { transactionControl: true });
|
|
182
|
+
}
|
|
183
|
+
const explainPool = new sql2.ConnectionPool({
|
|
184
|
+
...config,
|
|
185
|
+
pool: { ...config.pool, max: 1, min: 1 }
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
await explainPool.connect();
|
|
189
|
+
await explainPool.request().batch("SET STATISTICS XML ON");
|
|
190
|
+
let planResult;
|
|
191
|
+
if (options.readonly) {
|
|
192
|
+
planResult = await batchRolledBack(
|
|
193
|
+
explainPool,
|
|
194
|
+
innerQuery,
|
|
195
|
+
parameters
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
const planRequest = explainPool.request();
|
|
199
|
+
bindParameters(planRequest, parameters);
|
|
200
|
+
planResult = await planRequest.batch(innerQuery);
|
|
201
|
+
}
|
|
202
|
+
const planXml = extractPlanXml(planResult);
|
|
203
|
+
return {
|
|
204
|
+
resultSets: [
|
|
205
|
+
{
|
|
206
|
+
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
207
|
+
rowCount: planXml != null ? 1 : 0
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(`Failed to explain analyze query: ${error.message}`);
|
|
213
|
+
} finally {
|
|
214
|
+
await explainPool.close();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function batchRolledBack(pool, innerQuery, parameters) {
|
|
218
|
+
const transaction = new sql2.Transaction(pool);
|
|
219
|
+
await transaction.begin();
|
|
220
|
+
let queryFailed = false;
|
|
221
|
+
try {
|
|
222
|
+
const request = new sql2.Request(transaction);
|
|
223
|
+
bindParameters(request, parameters);
|
|
224
|
+
return await request.batch(innerQuery);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
queryFailed = true;
|
|
227
|
+
throw error;
|
|
228
|
+
} finally {
|
|
229
|
+
try {
|
|
230
|
+
await transaction.rollback();
|
|
231
|
+
} catch (rollbackError) {
|
|
232
|
+
if (!queryFailed) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Read-only rollback failed \u2014 data may have been modified: ${rollbackError.message}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function extractPlanXml(result) {
|
|
241
|
+
const recordsets = result.recordsets ?? [];
|
|
242
|
+
for (let i = recordsets.length - 1; i >= 0; i--) {
|
|
243
|
+
const firstRow = recordsets[i]?.[0];
|
|
244
|
+
if (!firstRow) continue;
|
|
245
|
+
const values = Object.values(firstRow);
|
|
246
|
+
if (values.length !== 1) continue;
|
|
247
|
+
const value = values[0];
|
|
248
|
+
if (typeof value === "string" && value.includes("<ShowPlanXML")) {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/connectors/sqlserver/index.ts
|
|
29
256
|
var SQLServerDSNParser = class {
|
|
30
257
|
async parse(dsn, config) {
|
|
31
258
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
@@ -40,13 +267,16 @@ Expected: ${expectedFormat}`
|
|
|
40
267
|
);
|
|
41
268
|
}
|
|
42
269
|
try {
|
|
270
|
+
const queryStart = dsn.indexOf("?");
|
|
271
|
+
const sslmodes = new URLSearchParams(queryStart === -1 ? "" : dsn.substring(queryStart + 1)).getAll("sslmode");
|
|
272
|
+
if (sslmodes.length > 1 || sslmodes.length === 1 && !["disable", "require", "verify-full"].includes(sslmodes[0])) {
|
|
273
|
+
throw new Error("Invalid sslmode. Specify exactly one value: disable, require, verify-full");
|
|
274
|
+
}
|
|
43
275
|
const url = new SafeURL(dsn);
|
|
44
|
-
const options = {};
|
|
276
|
+
const options = { sslmode: sslmodes[0] };
|
|
45
277
|
url.forEachSearchParam((value, key) => {
|
|
46
278
|
if (key === "authentication") {
|
|
47
279
|
options.authentication = value;
|
|
48
|
-
} else if (key === "sslmode") {
|
|
49
|
-
options.sslmode = value;
|
|
50
280
|
} else if (key === "instanceName") {
|
|
51
281
|
options.instanceName = value;
|
|
52
282
|
} else if (key === "domain") {
|
|
@@ -66,6 +296,9 @@ Expected: ${expectedFormat}`
|
|
|
66
296
|
} else if (options.sslmode === "require") {
|
|
67
297
|
options.encrypt = true;
|
|
68
298
|
options.trustServerCertificate = true;
|
|
299
|
+
} else if (options.sslmode === "verify-full") {
|
|
300
|
+
options.encrypt = true;
|
|
301
|
+
options.trustServerCertificate = false;
|
|
69
302
|
}
|
|
70
303
|
}
|
|
71
304
|
const config2 = {
|
|
@@ -169,7 +402,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
169
402
|
if (!this.config.options) {
|
|
170
403
|
this.config.options = {};
|
|
171
404
|
}
|
|
172
|
-
this.connection = new
|
|
405
|
+
this.connection = new sql3.ConnectionPool(this.config);
|
|
173
406
|
await this.connection.connect();
|
|
174
407
|
} catch (error) {
|
|
175
408
|
if (this.connection) {
|
|
@@ -207,7 +440,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
207
440
|
}
|
|
208
441
|
try {
|
|
209
442
|
const schemaToUse = schema || "dbo";
|
|
210
|
-
const request = this.connection.request().input("schema",
|
|
443
|
+
const request = this.connection.request().input("schema", sql3.VarChar, schemaToUse);
|
|
211
444
|
const query = `
|
|
212
445
|
SELECT TABLE_NAME
|
|
213
446
|
FROM INFORMATION_SCHEMA.TABLES
|
|
@@ -227,7 +460,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
227
460
|
}
|
|
228
461
|
try {
|
|
229
462
|
const schemaToUse = schema || "dbo";
|
|
230
|
-
const request = this.connection.request().input("schema",
|
|
463
|
+
const request = this.connection.request().input("schema", sql3.VarChar, schemaToUse);
|
|
231
464
|
const query = `
|
|
232
465
|
SELECT TABLE_NAME
|
|
233
466
|
FROM INFORMATION_SCHEMA.TABLES
|
|
@@ -247,7 +480,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
247
480
|
}
|
|
248
481
|
try {
|
|
249
482
|
const schemaToUse = schema || "dbo";
|
|
250
|
-
const request = this.connection.request().input("tableName",
|
|
483
|
+
const request = this.connection.request().input("tableName", sql3.VarChar, tableName).input("schema", sql3.VarChar, schemaToUse);
|
|
251
484
|
const query = `
|
|
252
485
|
SELECT COUNT(*) as count
|
|
253
486
|
FROM INFORMATION_SCHEMA.TABLES
|
|
@@ -266,7 +499,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
266
499
|
}
|
|
267
500
|
try {
|
|
268
501
|
const schemaToUse = schema || "dbo";
|
|
269
|
-
const request = this.connection.request().input("tableName",
|
|
502
|
+
const request = this.connection.request().input("tableName", sql3.VarChar, tableName).input("schema", sql3.VarChar, schemaToUse);
|
|
270
503
|
const query = `
|
|
271
504
|
SELECT i.name AS index_name,
|
|
272
505
|
i.is_unique,
|
|
@@ -324,7 +557,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
324
557
|
}
|
|
325
558
|
try {
|
|
326
559
|
const schemaToUse = schema || "dbo";
|
|
327
|
-
const request = this.connection.request().input("tableName",
|
|
560
|
+
const request = this.connection.request().input("tableName", sql3.VarChar, tableName).input("schema", sql3.VarChar, schemaToUse);
|
|
328
561
|
const query = `
|
|
329
562
|
SELECT c.COLUMN_NAME as column_name,
|
|
330
563
|
c.DATA_TYPE as data_type,
|
|
@@ -358,7 +591,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
358
591
|
}
|
|
359
592
|
try {
|
|
360
593
|
const schemaToUse = schema || "dbo";
|
|
361
|
-
const request = this.connection.request().input("tableName",
|
|
594
|
+
const request = this.connection.request().input("tableName", sql3.VarChar, tableName).input("schema", sql3.VarChar, schemaToUse);
|
|
362
595
|
const query = `
|
|
363
596
|
SELECT ep.value as table_comment
|
|
364
597
|
FROM sys.extended_properties ep
|
|
@@ -459,7 +692,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
459
692
|
}
|
|
460
693
|
try {
|
|
461
694
|
const schemaToUse = schema || "dbo";
|
|
462
|
-
const request = this.connection.request().input("schema",
|
|
695
|
+
const request = this.connection.request().input("schema", sql3.VarChar, schemaToUse);
|
|
463
696
|
let typeFilter;
|
|
464
697
|
if (routineType === "function") {
|
|
465
698
|
typeFilter = "AND ROUTINE_TYPE = 'FUNCTION'";
|
|
@@ -487,7 +720,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
487
720
|
}
|
|
488
721
|
try {
|
|
489
722
|
const schemaToUse = schema || "dbo";
|
|
490
|
-
const request = this.connection.request().input("procedureName",
|
|
723
|
+
const request = this.connection.request().input("procedureName", sql3.VarChar, procedureName).input("schema", sql3.VarChar, schemaToUse);
|
|
491
724
|
const routineQuery = `
|
|
492
725
|
SELECT ROUTINE_NAME as procedure_name,
|
|
493
726
|
ROUTINE_TYPE,
|
|
@@ -549,35 +782,41 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
549
782
|
}
|
|
550
783
|
}
|
|
551
784
|
async executeSQL(sqlQuery, options, parameters) {
|
|
552
|
-
if (!this.connection) {
|
|
785
|
+
if (!this.connection || !this.config) {
|
|
553
786
|
throw new Error("Not connected to SQL Server database");
|
|
554
787
|
}
|
|
555
788
|
const afterNoise = sqlQuery.replace(LEADING_SQL_NOISE, "");
|
|
556
789
|
if (/^explain\b/i.test(afterNoise)) {
|
|
557
|
-
const { analyze, query } =
|
|
790
|
+
const { analyze, query } = parseExplainPrefix(
|
|
558
791
|
afterNoise.slice("explain".length).trim()
|
|
559
792
|
);
|
|
560
|
-
return analyze ? this.
|
|
793
|
+
return analyze ? explainAnalyzeQuery(this.config, query, options, parameters) : explainQuery(this.config, query, options.readonly, parameters);
|
|
561
794
|
}
|
|
562
795
|
try {
|
|
796
|
+
const statements = splitSQLStatements(sqlQuery, "sqlserver");
|
|
797
|
+
const isSingleStatement = statements.length === 1;
|
|
563
798
|
let processedSQL = sqlQuery;
|
|
564
|
-
let probeApplied = false;
|
|
565
799
|
if (options.maxRows) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
800
|
+
const maxRows = options.maxRows;
|
|
801
|
+
if (isSingleStatement) {
|
|
802
|
+
processedSQL = SQLRowLimiter.applyMaxRowsForSQLServerWithTruncationProbe(sqlQuery, maxRows).sql;
|
|
803
|
+
} else {
|
|
804
|
+
const terminator = sqlQuery.trimEnd().endsWith(";") ? "\n;" : "";
|
|
805
|
+
const moduleStart = statements.findIndex(
|
|
806
|
+
(statement) => _SQLServerConnector.MODULE_DEFINITION.test(stripCommentsAndStrings(statement, "sqlserver"))
|
|
807
|
+
);
|
|
808
|
+
const rewritable = moduleStart === -1 ? statements.length : moduleStart;
|
|
809
|
+
processedSQL = statements.map(
|
|
810
|
+
(statement, i) => i < rewritable ? SQLRowLimiter.applyMaxRowsForSQLServerWithTruncationProbe(statement, maxRows).sql : statement
|
|
811
|
+
).join("\n;\n") + terminator;
|
|
812
|
+
}
|
|
572
813
|
}
|
|
573
|
-
const isSingleStatement = splitSQLStatements(processedSQL, "sqlserver").length === 1;
|
|
574
814
|
if (options.readonly) {
|
|
575
815
|
return await this.executeReadOnly(
|
|
576
816
|
processedSQL,
|
|
577
817
|
parameters,
|
|
578
818
|
isSingleStatement ? sqlQuery : void 0,
|
|
579
|
-
options.maxRows
|
|
580
|
-
probeApplied
|
|
819
|
+
options.maxRows
|
|
581
820
|
);
|
|
582
821
|
}
|
|
583
822
|
const request = this.connection.request();
|
|
@@ -594,16 +833,14 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
594
833
|
});
|
|
595
834
|
}
|
|
596
835
|
);
|
|
597
|
-
|
|
836
|
+
bindParameters(request, parameters);
|
|
598
837
|
const result = await request.query(processedSQL);
|
|
599
838
|
const resultSets = _SQLServerConnector.buildResultSets(
|
|
600
839
|
result.recordsets,
|
|
601
840
|
result.rowsAffected,
|
|
602
|
-
isSingleStatement ? sqlQuery : void 0
|
|
841
|
+
isSingleStatement ? sqlQuery : void 0,
|
|
842
|
+
options.maxRows
|
|
603
843
|
);
|
|
604
|
-
if (resultSets.length > 0) {
|
|
605
|
-
SQLRowLimiter.flagTruncation(resultSets[0], options.maxRows, probeApplied);
|
|
606
|
-
}
|
|
607
844
|
return {
|
|
608
845
|
resultSets,
|
|
609
846
|
...messages.length > 0 ? { messages } : {}
|
|
@@ -613,28 +850,18 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
613
850
|
}
|
|
614
851
|
}
|
|
615
852
|
/**
|
|
616
|
-
* Builds one result set per
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
* single summed result set, and a batch mixing writes with selects gets
|
|
627
|
-
* one result set per select plus one trailing "writes" set for the rest -
|
|
628
|
-
* not a truly per-statement breakdown, but no read statement's rows are
|
|
629
|
-
* ever merged with another's, which is what mattered for #380.
|
|
630
|
-
*
|
|
631
|
-
* `sourceSql`, when given, is attributed to the single resulting set - it
|
|
632
|
-
* must be omitted (pass `undefined`) unless the caller has already
|
|
633
|
-
* confirmed the batch is unambiguously one statement, since for a genuine
|
|
634
|
-
* multi-statement batch there's no reliable way to say which source
|
|
635
|
-
* statement a given recordset (or the trailing writes set) came from.
|
|
853
|
+
* Builds one result set per recordset. With `maxRows`, every set is capped:
|
|
854
|
+
* a statement the TOP/FETCH probe rewrite reached returns at most
|
|
855
|
+
* maxRows + 1 rows, and more than maxRows rows means the cap fired, so the
|
|
856
|
+
* probe row is dropped and the set flagged truncated. The same check also
|
|
857
|
+
* bounds result sets the rewrite could not reach (a stored procedure's
|
|
858
|
+
* output, say): those are trimmed to maxRows and flagged the same way. A
|
|
859
|
+
* statement whose own TOP/FETCH is within the cap never exceeds maxRows
|
|
860
|
+
* rows, so it is never flagged. Recordsets do not map 1:1 onto statements
|
|
861
|
+
* (a SELECT ... INTO returns none, an EXEC may return several), which is
|
|
862
|
+
* why the check is per result set rather than per rewritten statement.
|
|
636
863
|
*/
|
|
637
|
-
static buildResultSets(recordsets, rowsAffected, sourceSql) {
|
|
864
|
+
static buildResultSets(recordsets, rowsAffected, sourceSql, maxRows) {
|
|
638
865
|
const sets = (recordsets ?? []).map((recordset) => {
|
|
639
866
|
const rows = recordset ?? [];
|
|
640
867
|
return { rows, rowCount: rows.length };
|
|
@@ -642,6 +869,9 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
642
869
|
const totalAffected = (rowsAffected ?? []).reduce((total, count) => total + (count ?? 0), 0);
|
|
643
870
|
const accountedFor = sets.reduce((total, set) => total + set.rowCount, 0);
|
|
644
871
|
const writesOnly = totalAffected - accountedFor;
|
|
872
|
+
for (const set of sets) {
|
|
873
|
+
SQLRowLimiter.flagTruncation(set, maxRows, true);
|
|
874
|
+
}
|
|
645
875
|
if (sets.length === 0) {
|
|
646
876
|
sets.push({ rows: [], rowCount: totalAffected });
|
|
647
877
|
} else if (writesOnly > 0) {
|
|
@@ -652,78 +882,6 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
652
882
|
}
|
|
653
883
|
return sets;
|
|
654
884
|
}
|
|
655
|
-
/**
|
|
656
|
-
* Bind positional parameters as @p1, @p2, ... inferring the SQL Server type
|
|
657
|
-
* from each JavaScript value. Shared by every execution path so they cannot
|
|
658
|
-
* drift in how a given value is typed.
|
|
659
|
-
*
|
|
660
|
-
* Works for `batch` as well as `query`: node-mssql prepends the matching
|
|
661
|
-
* DECLARE/SET statements when the request is a batch.
|
|
662
|
-
*/
|
|
663
|
-
static bindParameters(request, parameters) {
|
|
664
|
-
if (!parameters || parameters.length === 0) {
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
parameters.forEach((param, index) => {
|
|
668
|
-
const paramName = `p${index + 1}`;
|
|
669
|
-
if (typeof param === "string") {
|
|
670
|
-
request.input(paramName, sql.VarChar, param);
|
|
671
|
-
} else if (typeof param === "number") {
|
|
672
|
-
if (Number.isInteger(param)) {
|
|
673
|
-
request.input(paramName, sql.Int, param);
|
|
674
|
-
} else {
|
|
675
|
-
request.input(paramName, sql.Float, param);
|
|
676
|
-
}
|
|
677
|
-
} else if (typeof param === "boolean") {
|
|
678
|
-
request.input(paramName, sql.Bit, param);
|
|
679
|
-
} else if (param === null || param === void 0) {
|
|
680
|
-
request.input(paramName, sql.VarChar, param);
|
|
681
|
-
} else if (Array.isArray(param)) {
|
|
682
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
683
|
-
} else {
|
|
684
|
-
request.input(paramName, sql.VarChar, JSON.stringify(param));
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Reject the constructs that escape SQL Server's read-only guards, for use by
|
|
690
|
-
* both read-only execution paths.
|
|
691
|
-
*
|
|
692
|
-
* - Dynamic SQL (sqlServerDynamicSqlKeywords): can carry hidden COMMIT/ROLLBACK
|
|
693
|
-
* inside string literals that stripCommentsAndStrings removes
|
|
694
|
-
* - Pass-through data sources (sqlServerPassThroughKeywords): execute on a
|
|
695
|
-
* remote or ad-hoc source, so a local rollback never reaches them
|
|
696
|
-
* - COMMIT/ROLLBACK, when `transactionControl` is set: would end the wrapping
|
|
697
|
-
* transaction, letting writes persist. Only meaningful for the transaction
|
|
698
|
-
* path; the EXPLAIN path opens no transaction of its own.
|
|
699
|
-
*
|
|
700
|
-
* Both keyword lists are imported from the read-only classifier rather than
|
|
701
|
-
* redeclared, so the classifier and these backstops cannot drift apart.
|
|
702
|
-
*
|
|
703
|
-
* Note the COMMIT/ROLLBACK check is SQL Server-only by design. MySQL/MariaDB
|
|
704
|
-
* wrap batches in a transaction too, but there `commit`, `prepare` and
|
|
705
|
-
* `execute` are absent from their allow-lists in allowedKeywords, and
|
|
706
|
-
* execute-sql.ts requires every split statement to pass the classifier — so a
|
|
707
|
-
* transaction-control statement can never reach their backstop.
|
|
708
|
-
*/
|
|
709
|
-
assertNoReadOnlyEscapes(sqlText, { transactionControl = false } = {}) {
|
|
710
|
-
const cleaned = stripCommentsAndStrings(sqlText, "sqlserver").toLowerCase();
|
|
711
|
-
if (transactionControl && /\b(?:commit|rollback)\b/.test(cleaned)) {
|
|
712
|
-
throw new Error(
|
|
713
|
-
"Read-only mode: transaction control statements (COMMIT, ROLLBACK) are not allowed"
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
if (sqlServerDynamicSqlPattern.test(cleaned)) {
|
|
717
|
-
throw new Error(
|
|
718
|
-
`Read-only mode: dynamic SQL execution (${sqlServerDynamicSqlKeywords.map((k) => k.toUpperCase()).join(", ")}) is not allowed`
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
if (sqlServerPassThroughPattern.test(cleaned)) {
|
|
722
|
-
throw new Error(
|
|
723
|
-
`Read-only mode: pass-through data sources (${sqlServerPassThroughKeywords.map((k) => k.toUpperCase()).join(", ")}) are not allowed`
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
885
|
/**
|
|
728
886
|
* Execute a query inside a transaction that always rolls back, preventing
|
|
729
887
|
* any modifications from persisting. SQL Server has no native READ ONLY
|
|
@@ -733,11 +891,11 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
733
891
|
* Dangerous constructs are rejected before the transaction opens; see
|
|
734
892
|
* assertNoReadOnlyEscapes.
|
|
735
893
|
*/
|
|
736
|
-
async executeReadOnly(processedSQL, parameters, sourceSql, maxRows
|
|
737
|
-
|
|
738
|
-
const transaction = new
|
|
894
|
+
async executeReadOnly(processedSQL, parameters, sourceSql, maxRows) {
|
|
895
|
+
assertNoReadOnlyEscapes(processedSQL, { transactionControl: true });
|
|
896
|
+
const transaction = new sql3.Transaction(this.connection);
|
|
739
897
|
await transaction.begin();
|
|
740
|
-
const request = new
|
|
898
|
+
const request = new sql3.Request(transaction);
|
|
741
899
|
const messages = [];
|
|
742
900
|
request.on(
|
|
743
901
|
"info",
|
|
@@ -750,7 +908,7 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
750
908
|
});
|
|
751
909
|
}
|
|
752
910
|
);
|
|
753
|
-
|
|
911
|
+
bindParameters(request, parameters);
|
|
754
912
|
let result;
|
|
755
913
|
let queryFailed = false;
|
|
756
914
|
try {
|
|
@@ -772,257 +930,39 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
772
930
|
const resultSets = _SQLServerConnector.buildResultSets(
|
|
773
931
|
result.recordsets,
|
|
774
932
|
result.rowsAffected,
|
|
775
|
-
sourceSql
|
|
933
|
+
sourceSql,
|
|
934
|
+
maxRows
|
|
776
935
|
);
|
|
777
|
-
if (resultSets.length > 0) {
|
|
778
|
-
SQLRowLimiter.flagTruncation(resultSets[0], maxRows, probeApplied);
|
|
779
|
-
}
|
|
780
936
|
return {
|
|
781
937
|
resultSets,
|
|
782
938
|
...messages.length > 0 ? { messages } : {}
|
|
783
939
|
};
|
|
784
940
|
}
|
|
785
|
-
/**
|
|
786
|
-
* Return the estimated execution plan for a query using SHOWPLAN_XML.
|
|
787
|
-
*
|
|
788
|
-
* SHOWPLAN_XML compiles the statement and returns its plan without executing
|
|
789
|
-
* it, but it has two constraints: `SET SHOWPLAN_XML ON` must be the only
|
|
790
|
-
* statement in its batch, and the setting is session scoped. The shared pool
|
|
791
|
-
* hands out a fresh connection per request() and an open transaction
|
|
792
|
-
* suppresses SHOWPLAN, so neither can carry the setting to a follow-up query.
|
|
793
|
-
*
|
|
794
|
-
* We therefore run the SET / query pair on a short-lived, single-connection
|
|
795
|
-
* pool built from the same config. The dedicated session keeps SHOWPLAN state
|
|
796
|
-
* off the shared pool, so a concurrent query can never land on a connection
|
|
797
|
-
* with SHOWPLAN enabled (which would return a plan instead of its results).
|
|
798
|
-
*/
|
|
799
|
-
async explainQuery(innerQuery, readonly, parameters) {
|
|
800
|
-
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
801
|
-
if (!cleaned) {
|
|
802
|
-
throw new Error("EXPLAIN requires a statement to analyze");
|
|
803
|
-
}
|
|
804
|
-
if (readonly) {
|
|
805
|
-
this.assertNoReadOnlyEscapes(innerQuery);
|
|
806
|
-
}
|
|
807
|
-
if (/\bset\s+showplan/i.test(cleaned)) {
|
|
808
|
-
throw new Error("EXPLAIN does not support SET SHOWPLAN statements");
|
|
809
|
-
}
|
|
810
|
-
if (!this.config) {
|
|
811
|
-
throw new Error("Not connected to SQL Server database");
|
|
812
|
-
}
|
|
813
|
-
const explainPool = new sql.ConnectionPool({
|
|
814
|
-
...this.config,
|
|
815
|
-
pool: { ...this.config.pool, max: 1, min: 1 }
|
|
816
|
-
});
|
|
817
|
-
try {
|
|
818
|
-
await explainPool.connect();
|
|
819
|
-
await explainPool.request().batch("SET SHOWPLAN_XML ON");
|
|
820
|
-
const planRequest = explainPool.request();
|
|
821
|
-
_SQLServerConnector.bindParameters(planRequest, parameters);
|
|
822
|
-
const planResult = await planRequest.batch(innerQuery);
|
|
823
|
-
const planRow = planResult.recordset?.[0];
|
|
824
|
-
const planXml = planRow ? Object.values(planRow)[0] : null;
|
|
825
|
-
return {
|
|
826
|
-
resultSets: [
|
|
827
|
-
{
|
|
828
|
-
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
829
|
-
rowCount: planXml != null ? 1 : 0
|
|
830
|
-
}
|
|
831
|
-
]
|
|
832
|
-
};
|
|
833
|
-
} catch (error) {
|
|
834
|
-
throw new Error(`Failed to explain query: ${error.message}`);
|
|
835
|
-
} finally {
|
|
836
|
-
await explainPool.close();
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* Splits the modifiers between EXPLAIN and its statement, covering the same
|
|
841
|
-
* forms the read-only classifier recognises (see utils/allowed-keywords.ts):
|
|
842
|
-
* `ANALYZE`, `ANALYZE VERBOSE`, `(ANALYZE)`, `(ANALYZE, BUFFERS)`,
|
|
843
|
-
* `(ANALYZE false)`.
|
|
844
|
-
*
|
|
845
|
-
* Only ANALYZE carries a meaning here — it selects STATISTICS XML over
|
|
846
|
-
* SHOWPLAN_XML. The rest are PostgreSQL planner knobs with no SQL Server
|
|
847
|
-
* counterpart, so they are refused by name: silently dropping an option would
|
|
848
|
-
* quietly hand back something other than what was asked for.
|
|
849
|
-
*/
|
|
850
|
-
static parseExplainPrefix(afterExplain) {
|
|
851
|
-
if (afterExplain.startsWith("(")) {
|
|
852
|
-
const close = afterExplain.indexOf(")");
|
|
853
|
-
if (close < 0) {
|
|
854
|
-
throw new Error("EXPLAIN option list is missing its closing ')'");
|
|
855
|
-
}
|
|
856
|
-
let analyze = false;
|
|
857
|
-
for (const part of afterExplain.slice(1, close).split(",")) {
|
|
858
|
-
const token = part.trim();
|
|
859
|
-
if (!token) continue;
|
|
860
|
-
const parsed = _SQLServerConnector.EXPLAIN_OPTION.exec(token);
|
|
861
|
-
if (!parsed) {
|
|
862
|
-
throw new Error(
|
|
863
|
-
`EXPLAIN option '${token}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
const name = parsed[1];
|
|
867
|
-
if (!/^analyze$/i.test(name)) {
|
|
868
|
-
throw new Error(
|
|
869
|
-
`EXPLAIN option '${name}' is not supported on SQL Server \u2014 only ANALYZE is.`
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
const value = parsed[2];
|
|
873
|
-
if (value === void 0 || _SQLServerConnector.EXPLAIN_ON.test(value)) {
|
|
874
|
-
analyze = true;
|
|
875
|
-
} else if (_SQLServerConnector.EXPLAIN_OFF.test(value)) {
|
|
876
|
-
analyze = false;
|
|
877
|
-
} else {
|
|
878
|
-
throw new Error(`EXPLAIN option 'ANALYZE' expects a boolean, got '${value}'.`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return { analyze, query: afterExplain.slice(close + 1).trim() };
|
|
882
|
-
}
|
|
883
|
-
const analyzeKeyword = /^analyze\b/i.exec(afterExplain);
|
|
884
|
-
if (!analyzeKeyword) {
|
|
885
|
-
return { analyze: false, query: afterExplain };
|
|
886
|
-
}
|
|
887
|
-
const rest = afterExplain.slice(analyzeKeyword[0].length).trim();
|
|
888
|
-
const disabled = _SQLServerConnector.EXPLAIN_BARE_DISABLED.exec(rest);
|
|
889
|
-
if (disabled) {
|
|
890
|
-
return { analyze: false, query: rest.slice(disabled[0].length).trim() };
|
|
891
|
-
}
|
|
892
|
-
const trailing = /^([A-Za-z_]+)\b/.exec(rest);
|
|
893
|
-
if (trailing && /^verbose$/i.test(trailing[1])) {
|
|
894
|
-
throw new Error(
|
|
895
|
-
"EXPLAIN option 'VERBOSE' is not supported on SQL Server \u2014 only ANALYZE is."
|
|
896
|
-
);
|
|
897
|
-
}
|
|
898
|
-
return { analyze: true, query: rest };
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Run a statement under SET STATISTICS XML and return its *actual* execution
|
|
902
|
-
* plan — the Postgres `EXPLAIN ANALYZE` contract.
|
|
903
|
-
*
|
|
904
|
-
* SHOWPLAN_XML (plain EXPLAIN) compiles without executing, so its plan carries
|
|
905
|
-
* only estimates. STATISTICS XML runs the statement, so the plan reports real
|
|
906
|
-
* row counts and execution counts. The flip side is that this path is *not*
|
|
907
|
-
* inherently read-only the way explainQuery is, so under `options.readonly` the
|
|
908
|
-
* statement runs inside a transaction that always rolls back.
|
|
909
|
-
*
|
|
910
|
-
* The dedicated single-connection pool serves the same purpose as in
|
|
911
|
-
* explainQuery: the STATISTICS XML session toggle must never leak onto a
|
|
912
|
-
* shared pool connection, where a concurrent query would inherit it.
|
|
913
|
-
*/
|
|
914
|
-
async explainAnalyzeQuery(innerQuery, options, parameters) {
|
|
915
|
-
const cleaned = stripCommentsAndStrings(innerQuery, "sqlserver").trim();
|
|
916
|
-
if (!cleaned) {
|
|
917
|
-
throw new Error("EXPLAIN ANALYZE requires a statement to analyze");
|
|
918
|
-
}
|
|
919
|
-
if (/\bset\s+statistics\b/i.test(cleaned)) {
|
|
920
|
-
throw new Error("EXPLAIN ANALYZE does not support SET STATISTICS statements");
|
|
921
|
-
}
|
|
922
|
-
if (/\bset\s+showplan\b/i.test(cleaned)) {
|
|
923
|
-
throw new Error("EXPLAIN ANALYZE does not support SET SHOWPLAN statements");
|
|
924
|
-
}
|
|
925
|
-
if (options.readonly) {
|
|
926
|
-
this.assertNoReadOnlyEscapes(innerQuery, { transactionControl: true });
|
|
927
|
-
}
|
|
928
|
-
if (!this.config) {
|
|
929
|
-
throw new Error("Not connected to SQL Server database");
|
|
930
|
-
}
|
|
931
|
-
const explainPool = new sql.ConnectionPool({
|
|
932
|
-
...this.config,
|
|
933
|
-
pool: { ...this.config.pool, max: 1, min: 1 }
|
|
934
|
-
});
|
|
935
|
-
try {
|
|
936
|
-
await explainPool.connect();
|
|
937
|
-
await explainPool.request().batch("SET STATISTICS XML ON");
|
|
938
|
-
let planResult;
|
|
939
|
-
if (options.readonly) {
|
|
940
|
-
planResult = await _SQLServerConnector.batchRolledBack(
|
|
941
|
-
explainPool,
|
|
942
|
-
innerQuery,
|
|
943
|
-
parameters
|
|
944
|
-
);
|
|
945
|
-
} else {
|
|
946
|
-
const planRequest = explainPool.request();
|
|
947
|
-
_SQLServerConnector.bindParameters(planRequest, parameters);
|
|
948
|
-
planResult = await planRequest.batch(innerQuery);
|
|
949
|
-
}
|
|
950
|
-
const planXml = _SQLServerConnector.extractPlanXml(planResult);
|
|
951
|
-
return {
|
|
952
|
-
resultSets: [
|
|
953
|
-
{
|
|
954
|
-
rows: planXml != null ? [{ plan: planXml }] : [],
|
|
955
|
-
rowCount: planXml != null ? 1 : 0
|
|
956
|
-
}
|
|
957
|
-
]
|
|
958
|
-
};
|
|
959
|
-
} catch (error) {
|
|
960
|
-
throw new Error(`Failed to explain analyze query: ${error.message}`);
|
|
961
|
-
} finally {
|
|
962
|
-
await explainPool.close();
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Run a batch inside a transaction that is always rolled back, so EXPLAIN
|
|
967
|
-
* ANALYZE can report a real plan without letting the statement's writes stick.
|
|
968
|
-
*/
|
|
969
|
-
static async batchRolledBack(pool, innerQuery, parameters) {
|
|
970
|
-
const transaction = new sql.Transaction(pool);
|
|
971
|
-
await transaction.begin();
|
|
972
|
-
let queryFailed = false;
|
|
973
|
-
try {
|
|
974
|
-
const request = new sql.Request(transaction);
|
|
975
|
-
_SQLServerConnector.bindParameters(request, parameters);
|
|
976
|
-
return await request.batch(innerQuery);
|
|
977
|
-
} catch (error) {
|
|
978
|
-
queryFailed = true;
|
|
979
|
-
throw error;
|
|
980
|
-
} finally {
|
|
981
|
-
try {
|
|
982
|
-
await transaction.rollback();
|
|
983
|
-
} catch (rollbackError) {
|
|
984
|
-
if (!queryFailed) {
|
|
985
|
-
throw new Error(
|
|
986
|
-
`Read-only rollback failed \u2014 data may have been modified: ${rollbackError.message}`
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
/**
|
|
993
|
-
* Pull the ShowPlanXML document out of a STATISTICS XML result.
|
|
994
|
-
*
|
|
995
|
-
* STATISTICS XML interleaves each statement's plan with that statement's own
|
|
996
|
-
* result sets, so the plan sits at no fixed index — and `recordset` (singular)
|
|
997
|
-
* would hand back the statement's data instead. Scan from the end for the
|
|
998
|
-
* first single-column row holding a plan document.
|
|
999
|
-
*/
|
|
1000
|
-
static extractPlanXml(result) {
|
|
1001
|
-
const recordsets = result.recordsets ?? [];
|
|
1002
|
-
for (let i = recordsets.length - 1; i >= 0; i--) {
|
|
1003
|
-
const firstRow = recordsets[i]?.[0];
|
|
1004
|
-
if (!firstRow) continue;
|
|
1005
|
-
const values = Object.values(firstRow);
|
|
1006
|
-
if (values.length !== 1) continue;
|
|
1007
|
-
const value = values[0];
|
|
1008
|
-
if (typeof value === "string" && value.includes("<ShowPlanXML")) {
|
|
1009
|
-
return value;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
return null;
|
|
1013
|
-
}
|
|
1014
941
|
};
|
|
1015
|
-
/** Boolean spellings PostgreSQL accepts for an EXPLAIN option. */
|
|
1016
|
-
_SQLServerConnector.EXPLAIN_ON = /^(?:true|on|1)$/i;
|
|
1017
|
-
_SQLServerConnector.EXPLAIN_OFF = /^(?:false|off|0)$/i;
|
|
1018
942
|
/**
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
943
|
+
* Builds one result set per SELECT-producing statement in the batch, plus
|
|
944
|
+
* (if any) a trailing result set summarizing the write-only statements.
|
|
945
|
+
*
|
|
946
|
+
* node-mssql only pushes a `recordsets` entry for statements that produce
|
|
947
|
+
* columns (SELECT); INSERT/UPDATE/DELETE statements in the same batch
|
|
948
|
+
* contribute to `rowsAffected` only, with no way to recover which
|
|
949
|
+
* `rowsAffected` entry belongs to which statement from the driver's final
|
|
950
|
+
* arrays (`recordsets`/`rowsAffected` aren't index-aligned - see the
|
|
951
|
+
* tedious `doneHandler`, which always pushes to `rowsAffected` but only
|
|
952
|
+
* conditionally to `recordsets`). So a batch of pure writes collapses to a
|
|
953
|
+
* single summed result set, and a batch mixing writes with selects gets
|
|
954
|
+
* one result set per select plus one trailing "writes" set for the rest -
|
|
955
|
+
* not a truly per-statement breakdown, but no read statement's rows are
|
|
956
|
+
* ever merged with another's, which is what mattered for #380.
|
|
957
|
+
*
|
|
958
|
+
* `sourceSql`, when given, is attributed to the single resulting set - it
|
|
959
|
+
* must be omitted (pass `undefined`) unless the caller has already
|
|
960
|
+
* confirmed the batch is unambiguously one statement, since for a genuine
|
|
961
|
+
* multi-statement batch there's no reliable way to say which source
|
|
962
|
+
* statement a given recordset (or the trailing writes set) came from.
|
|
1022
963
|
*/
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
_SQLServerConnector.EXPLAIN_BARE_DISABLED = /^(?:=\s*)?(?:false|off|0)\b/i;
|
|
964
|
+
/** A segment that opens a module whose body extends to the end of the batch. */
|
|
965
|
+
_SQLServerConnector.MODULE_DEFINITION = /^\s*(?:create|alter)\s+(?:or\s+alter\s+)?(?:proc|procedure|function|trigger)\b/i;
|
|
1026
966
|
var SQLServerConnector = _SQLServerConnector;
|
|
1027
967
|
var sqlServerConnector = new SQLServerConnector();
|
|
1028
968
|
ConnectorRegistry.register(sqlServerConnector);
|