node-mssql

repository·master·Indexed 25 days ago

https://github.com/tediousjs/node-mssql

A Microsoft SQL Server client for Node.js (version 9.1.1) that supports multiple TDS drivers, including the pure JavaScript Tedious driver and the native MSNodeSQLv8 driver. It provides features for connection pooling, async/await query execution, ES6 tagged template literals for SQL injection protection, and streaming for large result sets.

Tokens
20.9K
Snippets
48
Records
104
Agent score
78%

What's inside node-mssql

  1. How the global connection pool works

    master

    The library uses connection pooling to reuse database connections across multiple requests in a single Node.js process. The sql.connect() function manages a single global connection pool.

    Key behaviors:

    • Singleton Pattern: There can only be one global connection pool connected at a time. Subsequent calls to sql.connect() with different configurations will not create a new connection if a pool is already connected.
    • Idempotency: If the global pool is already connected, sql.connect() resolves immediately to the existing pool.
    • Lifecycle: Do not call sql.close() after individual queries, as this destroys the pool for the entire application. Only call sql.close() when the application is shutting down (e.g., in a CLI tool or CRON job).

    It is recommended to await or .then() the pool creation to ensure it is ready before executing queries.

    const sql = require('mssql')
    const config = { ... }
    
    // run a query against the global connection pool
    function runQuery(query) {
      // sql.connect() will return the existing global pool if it exists or create a new one if it doesn't
      return sql.connect(config).then((pool) => {
        return pool.query(query)
      })
    }
  2. How ConnectionPool manages TDS connections

    master

    A ConnectionPool instance manages a pool of TDS connections. When you create a Request, Transaction, or Prepared Statement, a connection is acquired from the pool and reserved. Once the action completes, the connection is released back to the pool. The pool includes built-in health checks that automatically replace dead connections.

    IMPORTANT: You must always attach an error listener to your connection pool. If a connection error occurs and no listener is attached, your application will crash with an uncaught error.

    const pool = new sql.ConnectionPool({ /* config */ })
  3. Handle Geography and Geometry data types

    master

    node-mssql includes built-in deserializers for SQL Server's Geography and Geometry CLR data types.

    Geography

    Geography results are returned as objects containing srid, version, points, figures, shapes, and segments.

    Important: When working with Geography points, use the lat and lng properties. Avoid using the x and y properties, as they are flipped for compatibility and may lead to incorrect coordinate mapping.

    Geometry

    Geometry results are returned as objects containing srid, version, points, figures, shapes, and segments. Unlike Geography, Geometry types consistently place x before y in their coordinate ordering.

    // Example Geography output structure
    {
      srid: 4326,
      version: 2,
      points: [
        Point { lat: 1, lng: 1, z: null, m: null },
        // ...
      ],
      // ...
    }
    
    // Example Geometry output structure
    {
      srid: 4326,
      version: 1,
      points: [
        Point { x: 1, y: 1, z: null, m: null },
        // ...
      ],
      // ...
    }
  4. How prepared statements work in node-mssql

    master

    The PreparedStatement class ensures that all executions of a specific statement occur on a single connection.

    Critical Lifecycle Rules:

    • Connection Reservation: Calling prepare acquires a connection from the pool. This connection is reserved until you call unprepare.
    • Resource Management: You must call unprepare when finished. Failing to do so will cause the connection pool to run out of available connections.
    • Transactions: You can create a prepared statement within a transaction (new sql.PreparedStatement(transaction)), but you cannot execute other requests in that transaction until unprepare is called.
    const ps = new sql.PreparedStatement(/* [pool], [options] */)
  5. Use Diagnostics Channel for telemetry and tracing

    master

    The node-mssql package publishes telemetry via Node.js diagnostics_channel. This allows APM tools to observe queries, connections, and internal events with near-zero overhead when no subscribers are active.

    Constants for all channels are exported via the CHANNELS object.

    Tracing Channels (Async Lifecycle)

    These wrap async operations and emit start, end, asyncStart, asyncEnd, and error events. Subscribe using the pattern tracing:<channel_name>:<event>.

    ConstantChannel nameWraps
    TRACE_QUERYmssql:queryrequest.query()
    TRACE_BATCHmssql:batchrequest.batch()
    TRACE_EXECUTEmssql:executerequest.execute()
    TRACE_BULKmssql:bulkrequest.bulk()
    TRACE_CONNECTmssql:connectpool.connect()
    TRACE_POOL_ACQUIREmssql:pool:acquirePool connection acquire (wait time)
    TRACE_PREPARED_STATEMENT_PREPAREmssql:prepared-statement:prepareps.prepare()
    TRACE_PREPARED_STATEMENT_EXECUTEmssql:prepared-statement:executeps.execute()

    Important Security Note: The command, procedure, or statement fields in trace contexts contain the SQL text sent to the server. Because node-mssql is parameterized-first, user values appear in parameters and not in the SQL text. However, avoid hard-coding credentials or PII as inline SQL literals, as they will appear verbatim in traces.

    const dc = require('node:diagnostics_channel')
    const { CHANNELS } = require('mssql')
    
    dc.subscribe(`tracing:${CHANNELS.TRACE_QUERY}:start`, ({ command, requestId }) => {
      console.log(`[${requestId}] Query: ${command}`)
    })
    
    dc.subscribe(`tracing:${CHANNELS.TRACE_QUERY}:error`, ({ requestId, error }) => {
      console.error(`[${requestId}] Failed:`, error.message)
    })
  6. Handle duplicate column names with arrayRowMode

    master

    By default, if a query returns multiple columns with the same name, node-mssql only returns metadata for the last column with that name, and values may be lost or incorrectly ordered.

    To resolve this, use the arrayRowMode configuration parameter. This can be set globally in the connection config or per-request on a sql.Request instance. When arrayRowMode is true, rows are returned as arrays of values, and a separate columns array provides the metadata for each index, preserving all columns even if they share names.

    const request = new sql.Request()
    request.arrayRowMode = true
    request
        .query("select 'asdf' as name, 'qwerty' as other_name, 'jkl' as name")
        .then(result => {
            console.log(result);
        });
    
    // When streaming with arrayRowMode enabled, the 'recordset' event 
    // returns an array of column metadata instead of a keyed object.
    request.stream = true
    request.arrayRowMode = true
    request.query("select 'asdf' as name, 'qwerty' as other_name, 'jkl' as name")
    request.on('recordset', recordset => console.log(recordset))
  7. Handle different types of node-mssql errors

    master

    The node-mssql module categorizes errors into four main types. When an error occurs, you can access the original error object via err.originalError if the stack has been cropped. If SQL Server generates multiple errors for a single request, you can access them via err.precedingErrors.

    Error Types

    • ConnectionError: Errors related to connections and the connection pool.
    • TransactionError: Errors related to creating, committing, or rolling back transactions.
    • RequestError: Errors related to query and stored procedure execution.
    • PreparedStatementError: Errors related to prepared statements.
  8. How transactions work in node-mssql

    master

    A Transaction ensures that all requests are executed on a single connection acquired from the connection pool. Once begin is called, a connection is reserved; once commit or rollback is called, the connection is released back to the pool.

    Key behaviors:

    • You can initialize a Transaction with a pool or use the global connection by omitting it.
    • You can provide an optional options object for per-transaction overrides (e.g., { requestTimeout: 60000 }). These are inherited by requests created from the transaction.
    • Requests can be created using new sql.Request(transaction) or transaction.request().
    • If XACT_ABORT is enabled, transactions might be aborted automatically by SQL Server. You should listen for the rollback event to handle these cases correctly.
    const transaction = new sql.Transaction(/* [pool], [options] */)
  9. Implement custom connection pool management

    master

    If you need to connect to multiple databases or separate pools for different operations (e.g., Read vs. Write), you should implement a custom pool manager using new sql.ConnectionPool(config).

    An effective manager typically uses a Map to cache pool instances by name, ensuring that each unique configuration is only connected once.

    // pool-manager.js
    const mssql = require('mssql')
    const pools = new Map();
    
    module.exports = {
     /**
      * Get or create a pool. If a pool doesn't exist the config must be provided.
      * If the pool does exist the config is ignored (even if it was different to the one provided
      * when creating the pool)
      *
      * @param {string} name
      * @param {{}} [config]
      * @return {Promise.<mssql.ConnectionPool>}
      */
     get: (name, config) => {
      if (!pools.has(name)) {
       if (!config) {
        throw new Error('Pool does not exist');
       }
       const pool = new mssql.ConnectionPool(config);
       // automatically remove the pool from the cache if `pool.close()` is called
       const close = pool.close.bind(pool);
       pool.close = (...args) => {
        pools.delete(name);
        return close(...args);
       }
       pools.set(name, pool.connect());
      }
      return pools.get(name);
     },
     /**
      * Closes all the pools and removes them from the store
      *
      * @return {Promise<mssql.ConnectionPool[]>}
      */
     closeAll: () => Promise.all(Array.from(pools.values()).map((connect) => {
      return connect.then((pool) => pool.close());
     })),
    };
  10. Prevent SQL injection

    master

    Always use parameters (request.input) or tagged template literals to pass values to your queries. This ensures values are sanitized and prevents SQL injection attacks.

    const request = new sql.Request()
    request.input('myval', sql.VarChar, '-- commented')
    request.query('select @myval as myval', (err, result) => {
        console.dir(result)
    })