mssql 12.3.1 → 12.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/README.md CHANGED
@@ -185,6 +185,7 @@ const config = {
185
185
  * [Metadata](#metadata)
186
186
  * [Data Types](#data-types)
187
187
  * [SQL injection](#sql-injection)
188
+ * [Diagnostics Channel](#diagnostics-channel)
188
189
  * [Contributing](https://github.com/tediousjs/node-mssql/wiki/Contributing)
189
190
  * [11.x to 12.x changes](#11x-to-12x-changes)
190
191
  * [10.x to 11.x changes](#10x-to-11x-changes)
@@ -855,11 +856,22 @@ const config = sql.ConnectionPool.parseConnectionString('Server=localhost,1433;D
855
856
  ## Request
856
857
 
857
858
  ```javascript
858
- const request = new sql.Request(/* [pool or transaction] */)
859
+ const request = new sql.Request(/* [pool or transaction], [options] */)
859
860
  ```
860
861
 
861
862
  If you omit pool/transaction argument, global pool is used instead.
862
863
 
864
+ The optional `options` argument allows per-request configuration overrides:
865
+
866
+ - **requestTimeout** - Override the pool's default request timeout (in ms) for this request only. This applies to queries and stored procedure executions; it does not apply to bulk data transfers (`request.bulk()`), which stream to completion as long as the connection is healthy. If you need to bound a bulk transfer, wrap the call with your own timer and call `request.cancel()`.
867
+
868
+ ```javascript
869
+ // Request with a 60-second timeout instead of the pool default
870
+ const request = new sql.Request(pool, { requestTimeout: 60000 })
871
+ ```
872
+
873
+ **Note:** When using the global pool, you must still pass `undefined` as the first argument to use options: `new sql.Request(undefined, { requestTimeout: 60000 })`.
874
+
863
875
  ### Events
864
876
 
865
877
  - **recordset(columns)** - Dispatched when metadata for new recordset are parsed.
@@ -1225,11 +1237,13 @@ request.cancel()
1225
1237
  **IMPORTANT:** always use `Transaction` class to create transactions - it ensures that all your requests are executed on one connection. Once you call `begin`, a single connection is acquired from the connection pool and all subsequent requests (initialized with the `Transaction` object) are executed exclusively on this connection. After you call `commit` or `rollback`, connection is then released back to the connection pool.
1226
1238
 
1227
1239
  ```javascript
1228
- const transaction = new sql.Transaction(/* [pool] */)
1240
+ const transaction = new sql.Transaction(/* [pool], [options] */)
1229
1241
  ```
1230
1242
 
1231
1243
  If you omit connection argument, global connection is used instead.
1232
1244
 
1245
+ The optional `options` argument allows per-transaction configuration overrides (e.g. `{ requestTimeout: 60000 }`). These are inherited by any requests created from this transaction unless overridden at the request level. Note that the timeout applies to data requests only, not to the `begin`/`commit`/`rollback` operations themselves.
1246
+
1233
1247
  __Example__
1234
1248
 
1235
1249
  ```javascript
@@ -1377,11 +1391,13 @@ __Errors__
1377
1391
  **IMPORTANT:** always use `PreparedStatement` class to create prepared statements - it ensures that all your executions of prepared statement are executed on one connection. Once you call `prepare`, a single connection is acquired from the connection pool and all subsequent executions are executed exclusively on this connection. After you call `unprepare`, the connection is then released back to the connection pool.
1378
1392
 
1379
1393
  ```javascript
1380
- const ps = new sql.PreparedStatement(/* [pool] */)
1394
+ const ps = new sql.PreparedStatement(/* [pool], [options] */)
1381
1395
  ```
1382
1396
 
1383
1397
  If you omit the connection argument, the global connection is used instead.
1384
1398
 
1399
+ The optional `options` argument allows per-statement configuration overrides (e.g. `{ requestTimeout: 60000 }`). The timeout is applied to the `prepare`, `execute`, and `unprepare` operations.
1400
+
1385
1401
  __Example__
1386
1402
 
1387
1403
  ```javascript
@@ -2220,6 +2236,103 @@ request.query('select @myval as myval', (err, result) => {
2220
2236
  })
2221
2237
  ```
2222
2238
 
2239
+ ## Diagnostics Channel
2240
+
2241
+ node-mssql publishes telemetry through Node.js [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html), enabling APM tools and custom instrumentation to observe queries, connections, and internal events without modifying application code. When no subscribers are active, overhead is near-zero.
2242
+
2243
+ All channel name constants are exported from the package:
2244
+
2245
+ ```js
2246
+ const { CHANNELS } = require('mssql')
2247
+ ```
2248
+
2249
+ ### TracingChannels (async lifecycle)
2250
+
2251
+ These use [`TracingChannel`](https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel) to wrap async operations, emitting `start`, `end`, `asyncStart`, `asyncEnd`, and `error` sub-events. Subscribe via `tracing:<name>:<event>`:
2252
+
2253
+ ```js
2254
+ const dc = require('node:diagnostics_channel')
2255
+ const { CHANNELS } = require('mssql')
2256
+
2257
+ dc.subscribe(`tracing:${CHANNELS.TRACE_QUERY}:start`, ({ command, requestId }) => {
2258
+ console.log(`[${requestId}] Query: ${command}`)
2259
+ })
2260
+
2261
+ dc.subscribe(`tracing:${CHANNELS.TRACE_QUERY}:error`, ({ requestId, error }) => {
2262
+ console.error(`[${requestId}] Failed:`, error.message)
2263
+ })
2264
+ ```
2265
+
2266
+ | Constant | Channel name | Wraps |
2267
+ |---|---|---|
2268
+ | `TRACE_QUERY` | `mssql:query` | `request.query()` |
2269
+ | `TRACE_BATCH` | `mssql:batch` | `request.batch()` |
2270
+ | `TRACE_EXECUTE` | `mssql:execute` | `request.execute()` |
2271
+ | `TRACE_BULK` | `mssql:bulk` | `request.bulk()` |
2272
+ | `TRACE_CONNECT` | `mssql:connect` | `pool.connect()` |
2273
+ | `TRACE_POOL_ACQUIRE` | `mssql:pool:acquire` | Pool connection acquire (wait time) |
2274
+ | `TRACE_PREPARED_STATEMENT_PREPARE` | `mssql:prepared-statement:prepare` | `ps.prepare()` |
2275
+ | `TRACE_PREPARED_STATEMENT_EXECUTE` | `mssql:prepared-statement:execute` | `ps.execute()` |
2276
+
2277
+ TracingChannel contexts include identifiers (`requestId`, `poolId`), operation details (SQL text, procedure name, parameter names), and — on completion — `result` or `error`. Parameter **values** are never included (only their names).
2278
+
2279
+ > **Note on SQL text in trace contexts:** `command` / `procedure` / `statement` fields contain the SQL text as sent to the server, to support OTel `db.query.text` conventions. Because node-mssql is parameterised-query-first, user-supplied values flow through `parameters` and do not appear in the SQL text. Avoid hard-coding credentials, tokens, or PII as inline SQL literals — anything hard-coded into a raw query will appear verbatim in trace contexts.
2280
+
2281
+ > **Note on identifiers:** `connectionId`, `poolId`, `requestId`, `transactionId`, and `preparedStatementId` are monotonically increasing integers scoped to the current node process. They are not stable across restarts and cannot be used to correlate activity across processes.
2282
+
2283
+ > **Note:** TracingChannel instrumentation fires for both the promise and callback APIs. The callback API is traced via Node's `TracingChannel#traceCallback`, which emits the same `start` / `end` / `asyncStart` / `asyncEnd` / `error` sub-events as the promise path, so subscribers do not need to branch by API style. Point-event channels (connection, transaction, pool lifecycle) likewise fire regardless of API style.
2284
+
2285
+ ### Point-event channels
2286
+
2287
+ These emit single events at state transitions via `dc.subscribe()`:
2288
+
2289
+ ```js
2290
+ dc.subscribe(CHANNELS.CONNECTION_RELEASE, ({ connectionId, poolId }) => {
2291
+ console.log(`Pool ${poolId}: connection ${connectionId} released`)
2292
+ })
2293
+ ```
2294
+
2295
+ | Constant | Channel name | Description |
2296
+ |---|---|---|
2297
+ | `CONNECTION_ACQUIRE` | `mssql:connection:acquire` | Connection borrowed from pool |
2298
+ | `CONNECTION_RELEASE` | `mssql:connection:release` | Connection returned to pool |
2299
+ | `CONNECTION_CREATE` | `mssql:connection:create` | New connection created in pool |
2300
+ | `CONNECTION_DESTROY` | `mssql:connection:destroy` | Connection destroyed |
2301
+ | `POOL_CLOSE` | `mssql:pool:close` | Pool closed (includes `reason`: `'closed'` or `'error'`; `error` on failure) |
2302
+ | `TRANSACTION_BEGIN` | `mssql:transaction:begin` | Transaction begun (includes numeric `isolationLevel` and `isolationLevelName`) |
2303
+ | `TRANSACTION_COMMIT` | `mssql:transaction:commit` | Transaction committed |
2304
+ | `TRANSACTION_ROLLBACK` | `mssql:transaction:rollback` | Transaction rolled back (includes `aborted` flag) |
2305
+ | `REQUEST_CANCEL` | `mssql:request:cancel` | Request cancelled |
2306
+ | `PREPARED_STATEMENT_UNPREPARE` | `mssql:prepared-statement:unprepare` | Prepared statement released |
2307
+
2308
+ ### Example: OpenTelemetry Spans
2309
+
2310
+ ```js
2311
+ const dc = require('node:diagnostics_channel')
2312
+ const { trace, SpanKind, SpanStatusCode } = require('@opentelemetry/api')
2313
+ const { CHANNELS } = require('mssql')
2314
+
2315
+ const tracer = trace.getTracer('mssql')
2316
+ const queryTC = dc.tracingChannel(CHANNELS.TRACE_QUERY)
2317
+
2318
+ queryTC.subscribe({
2319
+ start (ctx) {
2320
+ ctx.span = tracer.startSpan('mssql.query', {
2321
+ kind: SpanKind.CLIENT,
2322
+ attributes: { 'db.system': 'mssql', 'db.query.text': ctx.command },
2323
+ })
2324
+ },
2325
+ asyncEnd (ctx) { ctx.span?.end() },
2326
+ error (ctx) {
2327
+ if (ctx.span) {
2328
+ ctx.span.recordException(ctx.error)
2329
+ ctx.span.setStatus({ code: SpanStatusCode.ERROR })
2330
+ ctx.span.end()
2331
+ }
2332
+ },
2333
+ })
2334
+ ```
2335
+
2223
2336
  ## 11.x to 12.x changes
2224
2337
 
2225
2338
  - Config objects are no longer cloned by the library. Mutating a config object after passing it to a `ConnectionPool` results in undefined behaviour.
@@ -8,6 +8,7 @@ const { IDS } = require('../utils')
8
8
  const ConnectionError = require('../error/connection-error')
9
9
  const shared = require('../shared')
10
10
  const { MSSQLError } = require('../error')
11
+ const { CHANNELS, tracePromise, traceCallback, publish } = require('../diagnostics')
11
12
 
12
13
  /**
13
14
  * Class ConnectionPool.
@@ -371,10 +372,26 @@ class ConnectionPool extends EventEmitter {
371
372
  */
372
373
 
373
374
  acquire (requester, callback) {
374
- const acquirePromise = shared.Promise.resolve(this._acquire()).catch(err => {
375
- this.emit('error', err)
376
- throw err
377
- })
375
+ const requestId = IDS.get(requester)
376
+ const poolId = IDS.get(this)
377
+
378
+ const acquirePromise = tracePromise(CHANNELS.TRACE_POOL_ACQUIRE, () => {
379
+ return shared.Promise.resolve(this._acquire()).catch(err => {
380
+ this.emit('error', err)
381
+ throw err
382
+ }).then(connection => {
383
+ publish(CHANNELS.CONNECTION_ACQUIRE, () => ({
384
+ connectionId: IDS.get(connection),
385
+ requestId,
386
+ poolId
387
+ }))
388
+ return connection
389
+ })
390
+ }, () => ({
391
+ poolId,
392
+ requestId
393
+ }))
394
+
378
395
  if (typeof callback === 'function') {
379
396
  acquirePromise.then(connection => callback(null, connection, this.config)).catch(callback)
380
397
  return this
@@ -403,6 +420,11 @@ class ConnectionPool extends EventEmitter {
403
420
  release (connection) {
404
421
  debug('connection(%d): released', IDS.get(connection))
405
422
 
423
+ publish(CHANNELS.CONNECTION_RELEASE, () => ({
424
+ connectionId: IDS.get(connection),
425
+ poolId: IDS.get(this)
426
+ }))
427
+
406
428
  if (this.pool) {
407
429
  this.pool.release(connection)
408
430
  }
@@ -418,16 +440,36 @@ class ConnectionPool extends EventEmitter {
418
440
 
419
441
  connect (callback) {
420
442
  if (typeof callback === 'function') {
421
- this._connect(callback)
443
+ traceCallback(CHANNELS.TRACE_CONNECT, this._connect, 0, () => ({
444
+ server: this.config.server,
445
+ port: this.config.port,
446
+ database: this.config.database,
447
+ poolId: IDS.get(this),
448
+ poolConfig: {
449
+ min: (this.config.pool && this.config.pool.min) || 0,
450
+ max: (this.config.pool && this.config.pool.max) || 10
451
+ }
452
+ }), this, [callback])
422
453
  return this
423
454
  }
424
455
 
425
- return new shared.Promise((resolve, reject) => {
426
- return this._connect(err => {
427
- if (err) return reject(err)
428
- resolve(this)
456
+ return tracePromise(CHANNELS.TRACE_CONNECT, () => {
457
+ return new shared.Promise((resolve, reject) => {
458
+ return this._connect(err => {
459
+ if (err) return reject(err)
460
+ resolve(this)
461
+ })
429
462
  })
430
- })
463
+ }, () => ({
464
+ server: this.config.server,
465
+ port: this.config.port,
466
+ database: this.config.database,
467
+ poolId: IDS.get(this),
468
+ poolConfig: {
469
+ min: (this.config.pool && this.config.pool.min) || 0,
470
+ max: (this.config.pool && this.config.pool.max) || 10
471
+ }
472
+ }))
431
473
  }
432
474
 
433
475
  /**
@@ -559,11 +601,20 @@ class ConnectionPool extends EventEmitter {
559
601
 
560
602
  this.pool.destroy().then(() => {
561
603
  debug('pool(%d): pool closed, removing pool reference and executing close callbacks', IDS.get(this))
604
+ publish(CHANNELS.POOL_CLOSE, () => ({
605
+ poolId: IDS.get(this),
606
+ reason: 'closed'
607
+ }))
562
608
  this.pool = null
563
609
  this._closeStack.forEach(cb => {
564
610
  setImmediate(cb, null)
565
611
  })
566
612
  }).catch(err => {
613
+ publish(CHANNELS.POOL_CLOSE, () => ({
614
+ poolId: IDS.get(this),
615
+ reason: 'error',
616
+ error: err
617
+ }))
567
618
  this.pool = null
568
619
  this._closeStack.forEach(cb => {
569
620
  setImmediate(cb, err)
@@ -576,21 +627,23 @@ class ConnectionPool extends EventEmitter {
576
627
  /**
577
628
  * Returns new request using this connection.
578
629
  *
630
+ * @param {{ requestTimeout?: number }} [conf] Per-request overrides.
579
631
  * @return {Request}
580
632
  */
581
633
 
582
- request () {
583
- return new shared.driver.Request(this)
634
+ request (conf) {
635
+ return new shared.driver.Request(this, conf)
584
636
  }
585
637
 
586
638
  /**
587
639
  * Returns new transaction using this connection.
588
640
  *
641
+ * @param {{ requestTimeout?: number }} [conf] Per-transaction overrides, cascaded to child requests.
589
642
  * @return {Transaction}
590
643
  */
591
644
 
592
- transaction () {
593
- return new shared.driver.Transaction(this)
645
+ transaction (conf) {
646
+ return new shared.driver.Transaction(this, conf)
594
647
  }
595
648
 
596
649
  /**
package/lib/base/index.js CHANGED
@@ -10,6 +10,7 @@ const Table = require('../table')
10
10
  const ISOLATION_LEVEL = require('../isolationlevel')
11
11
  const { TYPES } = require('../datatypes')
12
12
  const { connect, close, on, off, removeListener, query, batch } = require('../global-connection')
13
+ const { CHANNELS } = require('../diagnostics')
13
14
 
14
15
  module.exports = {
15
16
  ConnectionPool,
@@ -31,6 +32,7 @@ module.exports = {
31
32
  Table,
32
33
  ISOLATION_LEVEL,
33
34
  TYPES,
35
+ CHANNELS,
34
36
  MAX: 65535, // (1 << 16) - 1
35
37
  map: shared.map,
36
38
  getTypeByValue: shared.getTypeByValue,
@@ -2,11 +2,12 @@
2
2
 
3
3
  const debug = require('debug')('mssql:base')
4
4
  const { EventEmitter } = require('node:events')
5
- const { IDS, objectHasProperty } = require('../utils')
5
+ const { IDS, objectHasProperty, getPoolId } = require('../utils')
6
6
  const globalConnection = require('../global-connection')
7
7
  const { TransactionError, PreparedStatementError } = require('../error')
8
8
  const shared = require('../shared')
9
9
  const { TYPES, declare } = require('../datatypes')
10
+ const { CHANNELS, tracePromise, traceCallback, publish } = require('../diagnostics')
10
11
 
11
12
  /**
12
13
  * Class PreparedStatement.
@@ -20,10 +21,11 @@ class PreparedStatement extends EventEmitter {
20
21
  /**
21
22
  * Creates a new Prepared Statement.
22
23
  *
23
- * @param {ConnectionPool|Transaction} [holder]
24
+ * @param {ConnectionPool|Transaction} [parent]
25
+ * @param {{ requestTimeout?: number }} [overrides]
24
26
  */
25
27
 
26
- constructor (parent) {
28
+ constructor (parent, overrides = {}) {
27
29
  super()
28
30
 
29
31
  IDS.add(this, 'PreparedStatement')
@@ -33,6 +35,10 @@ class PreparedStatement extends EventEmitter {
33
35
  this._handle = 0
34
36
  this.prepared = false
35
37
  this.parameters = {}
38
+ this.overrides = {}
39
+ if (Number.isFinite(overrides?.requestTimeout) && overrides.requestTimeout >= 0) {
40
+ this.overrides.requestTimeout = overrides.requestTimeout
41
+ }
36
42
  }
37
43
 
38
44
  get config () {
@@ -194,16 +200,28 @@ class PreparedStatement extends EventEmitter {
194
200
 
195
201
  prepare (statement, callback) {
196
202
  if (typeof callback === 'function') {
197
- this._prepare(statement, callback)
203
+ traceCallback(CHANNELS.TRACE_PREPARED_STATEMENT_PREPARE, this._prepare, 1, () => ({
204
+ statement: statement || this.statement,
205
+ parameters: Object.keys(this.parameters),
206
+ preparedStatementId: IDS.get(this),
207
+ poolId: getPoolId(this)
208
+ }), this, [statement, callback])
198
209
  return this
199
210
  }
200
211
 
201
- return new shared.Promise((resolve, reject) => {
202
- this._prepare(statement, err => {
203
- if (err) return reject(err)
204
- resolve(this)
212
+ return tracePromise(CHANNELS.TRACE_PREPARED_STATEMENT_PREPARE, () => {
213
+ return new shared.Promise((resolve, reject) => {
214
+ this._prepare(statement, err => {
215
+ if (err) return reject(err)
216
+ resolve(this)
217
+ })
205
218
  })
206
- })
219
+ }, () => ({
220
+ statement: statement || this.statement,
221
+ parameters: Object.keys(this.parameters),
222
+ preparedStatementId: IDS.get(this),
223
+ poolId: getPoolId(this)
224
+ }))
207
225
  }
208
226
 
209
227
  /**
@@ -232,7 +250,8 @@ class PreparedStatement extends EventEmitter {
232
250
  this._acquiredConnection = connection
233
251
  this._acquiredConfig = config
234
252
 
235
- const req = new shared.driver.Request(this)
253
+ const req = new shared.driver.Request(this, this.overrides)
254
+ req._internal = true
236
255
  req.stream = false
237
256
  req.output('handle', TYPES.Int)
238
257
  req.input('params', TYPES.NVarChar, ((() => {
@@ -276,15 +295,35 @@ class PreparedStatement extends EventEmitter {
276
295
 
277
296
  execute (values, callback) {
278
297
  if (this.stream || (typeof callback === 'function')) {
279
- return this._execute(values, callback)
298
+ if (typeof callback !== 'function') {
299
+ // Stream mode without a callback: no async boundary for traceCallback
300
+ // to hook — fall through to the untraced call. Subscribers interested
301
+ // in streaming completion should listen to Request events.
302
+ return this._execute(values, callback)
303
+ }
304
+ return traceCallback(CHANNELS.TRACE_PREPARED_STATEMENT_EXECUTE, this._execute, 1, () => ({
305
+ statement: this.statement,
306
+ parameters: Object.keys(this.parameters),
307
+ handle: this._handle,
308
+ preparedStatementId: IDS.get(this),
309
+ poolId: getPoolId(this)
310
+ }), this, [values, callback])
280
311
  }
281
312
 
282
- return new shared.Promise((resolve, reject) => {
283
- this._execute(values, (err, recordset) => {
284
- if (err) return reject(err)
285
- resolve(recordset)
313
+ return tracePromise(CHANNELS.TRACE_PREPARED_STATEMENT_EXECUTE, () => {
314
+ return new shared.Promise((resolve, reject) => {
315
+ this._execute(values, (err, recordset) => {
316
+ if (err) return reject(err)
317
+ resolve(recordset)
318
+ })
286
319
  })
287
- })
320
+ }, () => ({
321
+ statement: this.statement,
322
+ parameters: Object.keys(this.parameters),
323
+ handle: this._handle,
324
+ preparedStatementId: IDS.get(this),
325
+ poolId: getPoolId(this)
326
+ }))
288
327
  }
289
328
 
290
329
  /**
@@ -294,7 +333,8 @@ class PreparedStatement extends EventEmitter {
294
333
  */
295
334
 
296
335
  _execute (values, callback) {
297
- const req = new shared.driver.Request(this)
336
+ const req = new shared.driver.Request(this, this.overrides)
337
+ req._internal = true
298
338
  req.stream = this.stream
299
339
  req.arrayRowMode = this.arrayRowMode
300
340
  req.input('handle', TYPES.Int, this._handle)
@@ -334,13 +374,25 @@ class PreparedStatement extends EventEmitter {
334
374
 
335
375
  unprepare (callback) {
336
376
  if (typeof callback === 'function') {
337
- this._unprepare(callback)
377
+ this._unprepare(err => {
378
+ if (!err) {
379
+ publish(CHANNELS.PREPARED_STATEMENT_UNPREPARE, () => ({
380
+ preparedStatementId: IDS.get(this),
381
+ poolId: getPoolId(this)
382
+ }))
383
+ }
384
+ callback(err)
385
+ })
338
386
  return this
339
387
  }
340
388
 
341
389
  return new shared.Promise((resolve, reject) => {
342
390
  this._unprepare(err => {
343
391
  if (err) return reject(err)
392
+ publish(CHANNELS.PREPARED_STATEMENT_UNPREPARE, () => ({
393
+ preparedStatementId: IDS.get(this),
394
+ poolId: getPoolId(this)
395
+ }))
344
396
  resolve()
345
397
  })
346
398
  })
@@ -362,7 +414,8 @@ class PreparedStatement extends EventEmitter {
362
414
  return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG'))
363
415
  }
364
416
 
365
- const req = new shared.driver.Request(this)
417
+ const req = new shared.driver.Request(this, this.overrides)
418
+ req._internal = true
366
419
  req.stream = false
367
420
  req.input('handle', TYPES.Int, this._handle)
368
421
  req.execute('sp_unprepare', err => {