├── .commitlintrc ├── .devcontainer ├── .mssql.json ├── devcontainer.json └── docker-compose.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── gh-pages.yml │ └── nodejs.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.txt ├── LICENSE.md ├── README.md ├── bin └── mssql ├── index.js ├── lib ├── base │ ├── connection-pool.js │ ├── index.js │ ├── prepared-statement.js │ ├── request.js │ └── transaction.js ├── datatypes.js ├── error │ ├── connection-error.js │ ├── index.js │ ├── mssql-error.js │ ├── prepared-statement-error.js │ ├── request-error.js │ └── transaction-error.js ├── global-connection.js ├── isolationlevel.js ├── msnodesqlv8 │ ├── connection-pool.js │ ├── index.js │ ├── request.js │ └── transaction.js ├── shared.js ├── table.js ├── tedious │ ├── connection-pool.js │ ├── index.js │ ├── request.js │ └── transaction.js ├── udt.js └── utils.js ├── msnodesqlv8.js ├── package-lock.json ├── package.json ├── tedious.js └── test ├── cleanup.sql ├── common ├── cli.js ├── templatestring.js ├── tests.js ├── times.js ├── unit.js └── versionhelper.js ├── mocha.opts ├── msnodesqlv8 └── msnodesqlv8.js ├── prepare.sql └── tedious └── tedious.js /.commitlintrc: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | rules: 4 | body-max-line-length: [0] 5 | -------------------------------------------------------------------------------- /.devcontainer/.mssql.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "sa", 3 | "password": "yourStrong(!)Password", 4 | "server": "mssql", 5 | "port": 1433, 6 | "database": "master", 7 | "requestTimeout": 30000, 8 | "options": { 9 | "abortTransactionOnError": true, 10 | "encrypt": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tediousjs/node-mssql", 3 | 4 | "dockerComposeFile": "docker-compose.yml", 5 | "service": "app", 6 | 7 | "workspaceFolder": "/workspace", 8 | 9 | "settings": { 10 | "terminal.integrated.shell.linux": "/bin/bash" 11 | }, 12 | 13 | "extensions": [ 14 | "ms-mssql.mssql", 15 | "dbaeumer.vscode-eslint" 16 | ], 17 | 18 | "postCreateCommand": "cp -n .devcontainer/.mssql.json test/.mssql.json && npm install", 19 | 20 | "containerEnv": { 21 | "EDITOR": "code --wait" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | image: "mcr.microsoft.com/vscode/devcontainers/javascript-node:14" 6 | 7 | volumes: 8 | - "..:/workspace:cached" 9 | 10 | # Overrides default command so things don't shut down after the process ends. 11 | command: "sleep infinity" 12 | 13 | depends_on: 14 | - mssql 15 | 16 | mssql: 17 | image: "mcr.microsoft.com/mssql/server:2019-latest" 18 | 19 | restart: unless-stopped 20 | 21 | environment: 22 | - "ACCEPT_EULA=Y" 23 | - "SA_PASSWORD=yourStrong(!)Password" 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | ### Expected behaviour: 9 | 10 | 11 | 12 | ### Actual behaviour: 13 | 14 | 15 | 16 | ### Configuration: 17 | 18 | ``` 19 | // paste relevant config here 20 | ``` 21 | 22 | ### Software versions 23 | 24 | * NodeJS: 25 | * node-mssql: 26 | * SQL Server: 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | What this does: 2 | 3 | 4 | 5 | Related issues: 6 | 7 | 8 | 9 | Pre/Post merge checklist: 10 | 11 | - [ ] Update change log 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | - package-ecosystem: "npm" # See documentation for possible values 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | groups: 17 | release-tools: 18 | patterns: 19 | - "@commitlint/*" 20 | - "semantic-release" 21 | - "@semantic-release/*" 22 | test-tools: 23 | patterns: 24 | - "mocha" 25 | lint-tools: 26 | patterns: 27 | - "standard" 28 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Update GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 14 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: Setup Pages 26 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 27 | - name: Prep docs 28 | run: | 29 | mkdir ./docs 30 | cp ./README.md ./docs/index.md 31 | echo 'title: node-mssql' > docs/_config.yml 32 | echo 'description: Microsoft SQL Server client for Node.js' >> docs/_config.yml 33 | echo 'google_analytics: UA-42442367-2' >> docs/_config.yml 34 | echo 'show_downloads: true' >> docs/_config.yml 35 | echo 'remote_theme: pages-themes/cayman@v0.2.0' >> docs/_config.yml 36 | echo '' >> docs/_config.yml 37 | echo 'plugins:' >> docs/_config.yml 38 | echo ' - jekyll-remote-theme' >> docs/_config.yml 39 | echo ' - jekyll-mentions' >> docs/_config.yml 40 | - name: Build with Jekyll 41 | uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13 42 | with: 43 | source: ./docs 44 | destination: ./_site 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 47 | 48 | # Deployment job 49 | deploy: 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | runs-on: ubuntu-latest 54 | needs: build 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 59 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Lint, Test & Release 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | commitlint: 14 | name: Lint commits 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | with: 20 | fetch-depth: 0 21 | persist-credentials: false 22 | - name: Setup Node.js 23 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 24 | with: 25 | node-version: 18.x 26 | cache: 'npm' 27 | - name: Install dependencies 28 | run: npm clean-install 29 | - name: Lint commit 30 | if: github.event_name == 'push' 31 | run: npx commitlint --from HEAD~1 --to HEAD --verbose 32 | - name: Lint commits 33 | if: github.event_name == 'pull_request' 34 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 35 | codelint: 36 | name: Lint code 37 | runs-on: ubuntu-24.04 38 | steps: 39 | - name: Checkout code 40 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | persist-credentials: false 43 | - name: Setup Node.js 44 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 45 | with: 46 | node-version: 18.x 47 | cache: 'npm' 48 | - name: Install dependencies 49 | run: npm clean-install 50 | - name: Lint code 51 | run: npm run lint 52 | test-linux: 53 | name: Run tests 54 | runs-on: ${{ matrix.os }} 55 | services: 56 | sqlserver: 57 | image: mcr.microsoft.com/${{ matrix.sqlserver == 'edge' && 'azure-sql-edge' || 'mssql/server' }}:${{ matrix.sqlserver == 'edge' && 'latest' || format('{0}-latest', matrix.sqlserver ) }} 58 | ports: 59 | - 1433:1433 60 | env: 61 | ACCEPT_EULA: Y 62 | MSSQL_SA_PASSWORD: ${{ env.MSSQL_PASSWORD }} 63 | needs: 64 | - commitlint 65 | - codelint 66 | env: 67 | MSSQL_PASSWORD: 'yourStrong(!)Password' 68 | strategy: 69 | matrix: 70 | os: [ubuntu-24.04] 71 | node: [18.x, 20.x, 22.x, 24.x] 72 | sqlserver: [2019, 2022] 73 | steps: 74 | - name: Checkout code 75 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 76 | with: 77 | persist-credentials: false 78 | - name: Setup Node.js ${{ matrix.node }} 79 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 80 | with: 81 | node-version: ${{ matrix.node }} 82 | cache: 'npm' 83 | - name: Install dependencies 84 | run: npm clean-install 85 | - name: Run unit tests 86 | run: npm run test-unit 87 | - name: Store test config 88 | run: echo "{\"user\":\"sa\",\"password\":\"$MSSQL_PASSWORD\",\"server\":\"localhost\",\"port\":1433,\"database\":\"master\",\"options\":{\"trustServerCertificate\":true}}" > ./test/.mssql.json 89 | - name: Run tedious tests 90 | run: npm run test-tedious 91 | - name: Run cli tests 92 | run: npm run test-cli 93 | # The msnodesqlv8 tests fail with a segmentation fault 94 | # - name: Install OBDC 17 driver 95 | # run: | 96 | # sudo curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 97 | # sudo curl -o /etc/apt/sources.list.d/mssql-release.list https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/prod.list 98 | # sudo apt-get update 99 | # sudo apt-get install -y msodbcsql17 100 | # env: 101 | # ACCEPT_EULA: Y 102 | # - name: Install msnodesqlv8 103 | # run: npm install --no-save msnodesqlv8@^2 104 | # - name: Run msnodesqlv8 tests 105 | # run: npm run test-msnodesqlv8 106 | test-windows: 107 | name: Run tests 108 | needs: 109 | - commitlint 110 | - codelint 111 | - test-linux 112 | runs-on: ${{ matrix.os }} 113 | env: 114 | MSSQL_PASSWORD: 'yourStrong(!)Password' 115 | strategy: 116 | matrix: 117 | os: [windows-2019, windows-2022] 118 | node: [18.x, 20.x, 22.x, 24.x] 119 | sqlserver: [2008, 2012, 2014, 2016, 2017, 2019, 2022] 120 | # These sqlserver versions don't work on windows-2022 (at the moment) 121 | exclude: 122 | - os: windows-2022 123 | sqlserver: 2008 124 | - os: windows-2022 125 | sqlserver: 2012 126 | - os: windows-2022 127 | sqlserver: 2014 128 | steps: 129 | - name: Checkout code 130 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 131 | with: 132 | persist-credentials: false 133 | - name: Setup Node.js ${{ matrix.node }} 134 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 135 | with: 136 | node-version: ${{ matrix.node }} 137 | cache: 'npm' 138 | - name: Install dependencies 139 | run: npm clean-install 140 | - name: Run unit tests 141 | run: npm run test-unit 142 | - name: Setup SQL Server 143 | uses: tediousjs/setup-sqlserver@8ca7c5e60afa1a35f1fb19469fb68a18b8ed76ee # v2 144 | with: 145 | sqlserver-version: ${{ matrix.sqlserver }} 146 | sa-password: ${{ env.MSSQL_PASSWORD }} 147 | native-client-version: 11 148 | - name: Store test config 149 | shell: bash 150 | run: echo "{\"user\":\"sa\",\"password\":\"$MSSQL_PASSWORD\",\"server\":\"localhost\",\"port\":1433,\"database\":\"master\",\"requestTimeout\":30000,\"options\":{\"abortTransactionOnError\":true,\"encrypt\":false}}" > ./test/.mssql.json 151 | - name: Run tedious tests 152 | run: npm run test-tedious 153 | - name: Run cli tests 154 | run: npm run test-cli 155 | - name: Install msnodesqlv8 156 | if: ${{ matrix.node != '22.x' && matrix.node != '24.x' }} 157 | run: npm install --no-save msnodesqlv8@^2 158 | - name: Run msnodesqlv8 tests 159 | if: ${{ matrix.node != '22.x' && matrix.node != '24.x' }} 160 | run: npm run test-msnodesqlv8 161 | - name: Install msnodesqlv8 162 | if: ${{ matrix.node == '22.x' || matrix.node == '24.x' }} 163 | run: npm install --no-save msnodesqlv8@^4 164 | - name: Run msnodesqlv8 tests 165 | if: ${{ matrix.node == '22.x' && matrix.node == '24.x' }} 166 | run: npm run test-msnodesqlv8 167 | release: 168 | name: Release 169 | concurrency: release 170 | if: ${{ github.repository_owner == 'tediousjs' && github.event_name == 'push' && github.actor != 'dependabot[bot]' }} 171 | runs-on: ubuntu-24.04 172 | needs: 173 | - commitlint 174 | - codelint 175 | - test-linux 176 | - test-windows 177 | permissions: 178 | contents: write # to be able to publish a GitHub release 179 | issues: write # to be able to comment on released issues 180 | pull-requests: write # to be able to comment on released pull requests 181 | id-token: write # to enable use of OIDC for npm provenance 182 | steps: 183 | - name: Checkout 184 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 185 | with: 186 | fetch-depth: 0 187 | - name: Setup Node.js 188 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 189 | with: 190 | node-version: "18.x" 191 | cache: 'npm' 192 | - name: Install dependencies 193 | run: npm clean-install 194 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 195 | run: npm audit signatures 196 | - name: Release 197 | env: 198 | NPM_CONFIG_PROVENANCE: true 199 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 200 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 201 | run: npx semantic-release 202 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | /test/.mssql.json 4 | /test.js 5 | .vscode -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | preset: 'conventionalcommits' 2 | plugins: 3 | - '@semantic-release/commit-analyzer' 4 | - '@semantic-release/release-notes-generator' 5 | - '@semantic-release/npm' 6 | - '@semantic-release/github' 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2018 Patrik Simek and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bin/mssql: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { Command } = require('commander') 3 | const { version } = require('../package.json') 4 | const { resolve: resolvePath } = require('node:path') 5 | const { constants: { R_OK } } = require('node:fs') 6 | const { ConnectionPool } = require('../') 7 | const { lstat, access, readFile } = require('node:fs/promises') 8 | Buffer.prototype.toJSON = () => { 9 | return `0x${this.toString('hex')}` 10 | } 11 | 12 | /** 13 | * @param {Readable} stream 14 | * @returns {Promise} 15 | */ 16 | async function streamToBuffer (stream) { 17 | const chunks = [] 18 | return new Promise((resolve, reject) => { 19 | stream.on('data', (chunk) => { 20 | chunks.push(chunk) 21 | }) 22 | stream.on('end', () => { 23 | resolve(Buffer.concat(chunks)) 24 | }) 25 | stream.on('error', reject) 26 | }) 27 | } 28 | 29 | async function resolveConfig (opts, cfgFile) { 30 | const cfg = Object.entries({ 31 | options: { 32 | encrypt: opts.encrypt, 33 | trustServerCertificate: opts.trustServerCertificate 34 | }, 35 | user: opts.user, 36 | password: opts.password, 37 | server: opts.server, 38 | database: opts.database, 39 | port: opts.port 40 | }).reduce((config, [key, value]) => { 41 | if (value) { 42 | Object.assign(config, { 43 | [key]: value 44 | }) 45 | } 46 | return config 47 | }, {}) 48 | let cfgPath = cfgFile || process.cwd() 49 | const stat = await lstat(resolvePath(cfgPath)) 50 | if (stat.isDirectory()) { 51 | cfgPath = resolvePath(cfgPath, opts.config) 52 | } 53 | const configAccess = await access(cfgPath, R_OK).then(() => true).catch(() => false) 54 | if (!configAccess) { 55 | return cfg; 56 | } 57 | const config = await (readFile(cfgPath)) 58 | .then((content) => JSON.parse(content.toString())) 59 | 60 | return { 61 | ...config, 62 | ...cfg, 63 | options: { 64 | ...(config.options || {}), 65 | ...cfg.options 66 | } 67 | } 68 | } 69 | 70 | const program = new Command() 71 | 72 | program 73 | .name('mssql') 74 | .argument('[configPath]') 75 | .description('CLI tools for node-mssql') 76 | .version(version) 77 | .option('--config ', 'Configuration file for the connection', './.mssql.json') 78 | .option('--user ', 'User for the database connection') 79 | .option('--password ', 'Password for the database connection') 80 | .option('--server ', 'Server for the database connection') 81 | .option('--database ', 'Database for the database connection') 82 | .option('--port ', 'Port for the database connection', parseInt) 83 | .option('--encrypt', 'Use the encrypt option for this connection', false) 84 | .option('--trust-server-certificate', 'Trust the server certificate for this connection', false) 85 | // .option('--format ', 'The output format to use, eg: JSON', 'json') 86 | .action(async function (configPath, opts) { 87 | const [config, statement] = await Promise.all([ 88 | resolveConfig(opts, configPath), 89 | streamToBuffer(process.stdin).then((stmt) => stmt.toString().trim()) 90 | ]) 91 | if (!statement.length) { 92 | throw new Error('Statement is empty.') 93 | } 94 | const pool = await (new ConnectionPool(config)).connect() 95 | const request = pool.request() 96 | request.stream = true 97 | let started = false 98 | request.on('error', (e) => { 99 | pool.close() 100 | throw e 101 | }) 102 | request.on('recordset', () => { 103 | if (started) { 104 | process.stdout.write('],') 105 | } else { 106 | process.stdout.write('[') 107 | } 108 | started = false 109 | }) 110 | request.on('row', (row) => { 111 | if (!started) { 112 | started = true 113 | process.stdout.write('[') 114 | } else { 115 | process.stdout.write(',') 116 | } 117 | process.stdout.write(JSON.stringify(row)) 118 | }) 119 | request.on('done', () => { 120 | if (started) { 121 | process.stdout.write(']]') 122 | } 123 | process.stdout.write('\n') 124 | pool.close() 125 | }) 126 | request.query(statement) 127 | }) 128 | 129 | program.parseAsync(process.argv).catch((e) => { 130 | program.error(e.message, { exitCode: 1 }); 131 | }) 132 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/tedious') 4 | -------------------------------------------------------------------------------- /lib/base/connection-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { EventEmitter } = require('node:events') 4 | const debug = require('debug')('mssql:base') 5 | const { parseSqlConnectionString } = require('@tediousjs/connection-string') 6 | const tarn = require('tarn') 7 | const { IDS } = require('../utils') 8 | const ConnectionError = require('../error/connection-error') 9 | const shared = require('../shared') 10 | const clone = require('rfdc/default') 11 | const { MSSQLError } = require('../error') 12 | 13 | /** 14 | * Class ConnectionPool. 15 | * 16 | * Internally, each `Connection` instance is a separate pool of TDS connections. Once you create a new `Request`/`Transaction`/`Prepared Statement`, a new TDS connection is acquired from the pool and reserved for desired action. Once the action is complete, connection is released back to the pool. 17 | * 18 | * @property {Boolean} connected If true, connection is established. 19 | * @property {Boolean} connecting If true, connection is being established. 20 | * 21 | * @fires ConnectionPool#connect 22 | * @fires ConnectionPool#close 23 | */ 24 | 25 | class ConnectionPool extends EventEmitter { 26 | /** 27 | * Create new Connection. 28 | * 29 | * @param {Object|String} config Connection configuration object or connection string. 30 | * @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred. 31 | */ 32 | 33 | constructor (config, callback) { 34 | super() 35 | 36 | IDS.add(this, 'ConnectionPool') 37 | debug('pool(%d): created', IDS.get(this)) 38 | 39 | this._connectStack = [] 40 | this._closeStack = [] 41 | 42 | this._connected = false 43 | this._connecting = false 44 | this._healthy = false 45 | 46 | if (typeof config === 'string') { 47 | try { 48 | this.config = this.constructor.parseConnectionString(config) 49 | } catch (ex) { 50 | if (typeof callback === 'function') { 51 | return setImmediate(callback, ex) 52 | } 53 | throw ex 54 | } 55 | } else { 56 | this.config = clone(config) 57 | } 58 | 59 | // set defaults 60 | this.config.port = this.config.port || 1433 61 | this.config.options = this.config.options || {} 62 | this.config.stream = this.config.stream || false 63 | this.config.parseJSON = this.config.parseJSON || false 64 | this.config.arrayRowMode = this.config.arrayRowMode || false 65 | this.config.validateConnection = 'validateConnection' in this.config ? this.config.validateConnection : true 66 | 67 | const namedServer = /^(.*)\\(.*)$/.exec(this.config.server) 68 | if (namedServer) { 69 | this.config.server = namedServer[1] 70 | this.config.options.instanceName = namedServer[2] 71 | } 72 | 73 | if (typeof this.config.options.useColumnNames !== 'undefined' && this.config.options.useColumnNames !== true) { 74 | const ex = new MSSQLError('Invalid options `useColumnNames`, use `arrayRowMode` instead') 75 | if (typeof callback === 'function') { 76 | return setImmediate(callback, ex) 77 | } 78 | throw ex 79 | } 80 | 81 | if (typeof callback === 'function') { 82 | this.connect(callback) 83 | } 84 | } 85 | 86 | get connected () { 87 | return this._connected 88 | } 89 | 90 | get connecting () { 91 | return this._connecting 92 | } 93 | 94 | get healthy () { 95 | return this._healthy 96 | } 97 | 98 | static parseConnectionString (connectionString) { 99 | return this._parseConnectionString(connectionString) 100 | } 101 | 102 | static _parseAuthenticationType (type, entries) { 103 | switch (type.toLowerCase()) { 104 | case 'active directory integrated': 105 | if (entries.includes('token')) { 106 | return 'azure-active-directory-access-token' 107 | } else if (['client id', 'client secret', 'tenant id'].every(entry => entries.includes(entry))) { 108 | return 'azure-active-directory-service-principal-secret' 109 | } else if (['client id', 'msi endpoint', 'msi secret'].every(entry => entries.includes(entry))) { 110 | return 'azure-active-directory-msi-app-service' 111 | } else if (['client id', 'msi endpoint'].every(entry => entries.includes(entry))) { 112 | return 'azure-active-directory-msi-vm' 113 | } 114 | return 'azure-active-directory-default' 115 | case 'active directory password': 116 | return 'azure-active-directory-password' 117 | case 'ntlm': 118 | return 'ntlm' 119 | default: 120 | return 'default' 121 | } 122 | } 123 | 124 | static _parseConnectionString (connectionString) { 125 | const parsed = parseSqlConnectionString(connectionString, true, true) 126 | return Object.entries(parsed).reduce((config, [key, value]) => { 127 | switch (key) { 128 | case 'application name': 129 | break 130 | case 'applicationintent': 131 | Object.assign(config.options, { 132 | readOnlyIntent: value === 'readonly' 133 | }) 134 | break 135 | case 'asynchronous processing': 136 | break 137 | case 'attachdbfilename': 138 | break 139 | case 'authentication': 140 | Object.assign(config, { 141 | authentication_type: this._parseAuthenticationType(value, Object.keys(parsed)) 142 | }) 143 | break 144 | case 'column encryption setting': 145 | break 146 | case 'connection timeout': 147 | Object.assign(config, { 148 | connectionTimeout: value * 1000 149 | }) 150 | break 151 | case 'connection lifetime': 152 | break 153 | case 'connectretrycount': 154 | break 155 | case 'connectretryinterval': 156 | Object.assign(config.options, { 157 | connectionRetryInterval: value * 1000 158 | }) 159 | break 160 | case 'context connection': 161 | break 162 | case 'client id': 163 | Object.assign(config, { 164 | clientId: value 165 | }) 166 | break 167 | case 'client secret': 168 | Object.assign(config, { 169 | clientSecret: value 170 | }) 171 | break 172 | case 'current language': 173 | Object.assign(config.options, { 174 | language: value 175 | }) 176 | break 177 | case 'data source': 178 | { 179 | let server = value 180 | let instanceName 181 | let port = 1433 182 | if (/^np:/i.test(server)) { 183 | throw new Error('Connection via Named Pipes is not supported.') 184 | } 185 | if (/^tcp:/i.test(server)) { 186 | server = server.substr(4) 187 | } 188 | const namedServerParts = /^(.*)\\(.*)$/.exec(server) 189 | if (namedServerParts) { 190 | server = namedServerParts[1].trim() 191 | instanceName = namedServerParts[2].trim() 192 | } 193 | const serverParts = /^(.*),(.*)$/.exec(server) 194 | if (serverParts) { 195 | server = serverParts[1].trim() 196 | port = parseInt(serverParts[2].trim(), 10) 197 | } else { 198 | const instanceParts = /^(.*),(.*)$/.exec(instanceName) 199 | if (instanceParts) { 200 | instanceName = instanceParts[1].trim() 201 | port = parseInt(instanceParts[2].trim(), 10) 202 | } 203 | } 204 | if (server === '.' || server === '(.)' || server.toLowerCase() === '(localdb)' || server.toLowerCase() === '(local)') { 205 | server = 'localhost' 206 | } 207 | Object.assign(config, { 208 | port, 209 | server 210 | }) 211 | if (instanceName) { 212 | Object.assign(config.options, { 213 | instanceName 214 | }) 215 | } 216 | break 217 | } 218 | case 'encrypt': 219 | Object.assign(config.options, { 220 | encrypt: !!value 221 | }) 222 | break 223 | case 'enlist': 224 | break 225 | case 'failover partner': 226 | break 227 | case 'initial catalog': 228 | Object.assign(config, { 229 | database: value 230 | }) 231 | break 232 | case 'integrated security': 233 | break 234 | case 'max pool size': 235 | Object.assign(config.pool, { 236 | max: value 237 | }) 238 | break 239 | case 'min pool size': 240 | Object.assign(config.pool, { 241 | min: value 242 | }) 243 | break 244 | case 'msi endpoint': 245 | Object.assign(config, { 246 | msiEndpoint: value 247 | }) 248 | break 249 | case 'msi secret': 250 | Object.assign(config, { 251 | msiSecret: value 252 | }) 253 | break 254 | case 'multipleactiveresultsets': 255 | break 256 | case 'multisubnetfailover': 257 | Object.assign(config.options, { 258 | multiSubnetFailover: value 259 | }) 260 | break 261 | case 'network library': 262 | break 263 | case 'packet size': 264 | Object.assign(config.options, { 265 | packetSize: value 266 | }) 267 | break 268 | case 'password': 269 | Object.assign(config, { 270 | password: value 271 | }) 272 | break 273 | case 'persist security info': 274 | break 275 | case 'poolblockingperiod': 276 | break 277 | case 'pooling': 278 | break 279 | case 'replication': 280 | break 281 | case 'tenant id': 282 | Object.assign(config, { 283 | tenantId: value 284 | }) 285 | break 286 | case 'token': 287 | Object.assign(config, { 288 | token: value 289 | }) 290 | break 291 | case 'transaction binding': 292 | Object.assign(config.options, { 293 | enableImplicitTransactions: value.toLowerCase() === 'implicit unbind' 294 | }) 295 | break 296 | case 'transparentnetworkipresolution': 297 | break 298 | case 'trustservercertificate': 299 | Object.assign(config.options, { 300 | trustServerCertificate: value 301 | }) 302 | break 303 | case 'type system version': 304 | break 305 | case 'user id': { 306 | let user = value 307 | let domain 308 | const domainUser = /^(.*)\\(.*)$/.exec(user) 309 | if (domainUser) { 310 | domain = domainUser[1] 311 | user = domainUser[2] 312 | } 313 | if (domain) { 314 | Object.assign(config, { 315 | domain 316 | }) 317 | } 318 | if (user) { 319 | Object.assign(config, { 320 | user 321 | }) 322 | } 323 | break 324 | } 325 | case 'user instance': 326 | break 327 | case 'workstation id': 328 | Object.assign(config.options, { 329 | workstationId: value 330 | }) 331 | break 332 | case 'request timeout': 333 | Object.assign(config, { 334 | requestTimeout: parseInt(value, 10) 335 | }) 336 | break 337 | case 'stream': 338 | Object.assign(config, { 339 | stream: !!value 340 | }) 341 | break 342 | case 'useutc': 343 | Object.assign(config.options, { 344 | useUTC: !!value 345 | }) 346 | break 347 | case 'parsejson': 348 | Object.assign(config, { 349 | parseJSON: !!value 350 | }) 351 | break 352 | } 353 | return config 354 | }, { options: {}, pool: {} }) 355 | } 356 | 357 | /** 358 | * Acquire connection from this connection pool. 359 | * 360 | * @param {ConnectionPool|Transaction|PreparedStatement} requester Requester. 361 | * @param {acquireCallback} [callback] A callback which is called after connection has been acquired, or an error has occurred. If omited, method returns Promise. 362 | * @return {ConnectionPool|Promise} 363 | */ 364 | 365 | acquire (requester, callback) { 366 | const acquirePromise = shared.Promise.resolve(this._acquire()).catch(err => { 367 | this.emit('error', err) 368 | throw err 369 | }) 370 | if (typeof callback === 'function') { 371 | acquirePromise.then(connection => callback(null, connection, this.config)).catch(callback) 372 | return this 373 | } 374 | 375 | return acquirePromise 376 | } 377 | 378 | _acquire () { 379 | if (!this.pool) { 380 | return shared.Promise.reject(new ConnectionError('Connection not yet open.', 'ENOTOPEN')) 381 | } else if (this.pool.destroyed) { 382 | return shared.Promise.reject(new ConnectionError('Connection is closing', 'ENOTOPEN')) 383 | } 384 | 385 | return this.pool.acquire().promise 386 | } 387 | 388 | /** 389 | * Release connection back to the pool. 390 | * 391 | * @param {Connection} connection Previously acquired connection. 392 | * @return {ConnectionPool} 393 | */ 394 | 395 | release (connection) { 396 | debug('connection(%d): released', IDS.get(connection)) 397 | 398 | if (this.pool) { 399 | this.pool.release(connection) 400 | } 401 | return this 402 | } 403 | 404 | /** 405 | * Creates a new connection pool with one active connection. This one initial connection serves as a probe to find out whether the configuration is valid. 406 | * 407 | * @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise. 408 | * @return {ConnectionPool|Promise} 409 | */ 410 | 411 | connect (callback) { 412 | if (typeof callback === 'function') { 413 | this._connect(callback) 414 | return this 415 | } 416 | 417 | return new shared.Promise((resolve, reject) => { 418 | return this._connect(err => { 419 | if (err) return reject(err) 420 | resolve(this) 421 | }) 422 | }) 423 | } 424 | 425 | /** 426 | * @private 427 | * @param {basicCallback} callback 428 | */ 429 | 430 | _connect (callback) { 431 | if (this._connected) { 432 | debug('pool(%d): already connected, executing connect callback immediately', IDS.get(this)) 433 | return setImmediate(callback, null, this) 434 | } 435 | 436 | this._connectStack.push(callback) 437 | 438 | if (this._connecting) { 439 | return 440 | } 441 | 442 | this._connecting = true 443 | debug('pool(%d): connecting', IDS.get(this)) 444 | 445 | // create one test connection to check if everything is ok 446 | this._poolCreate().then((connection) => { 447 | debug('pool(%d): connected', IDS.get(this)) 448 | this._healthy = true 449 | 450 | return this._poolDestroy(connection).then(() => { 451 | // prepare pool 452 | this.pool = new tarn.Pool( 453 | Object.assign({ 454 | create: () => this._poolCreate() 455 | .then(connection => { 456 | this._healthy = true 457 | return connection 458 | }) 459 | .catch(err => { 460 | if (this.pool.numUsed() + this.pool.numFree() <= 0) { 461 | this._healthy = false 462 | } 463 | throw err 464 | }), 465 | validate: this._poolValidate.bind(this), 466 | destroy: this._poolDestroy.bind(this), 467 | max: 10, 468 | min: 0, 469 | idleTimeoutMillis: 30000, 470 | propagateCreateError: true 471 | }, this.config.pool) 472 | ) 473 | 474 | this._connecting = false 475 | this._connected = true 476 | }) 477 | }).then(() => { 478 | this._connectStack.forEach((cb) => { 479 | setImmediate(cb, null, this) 480 | }) 481 | }).catch(err => { 482 | this._connecting = false 483 | this._connectStack.forEach((cb) => { 484 | setImmediate(cb, err) 485 | }) 486 | }).then(() => { 487 | this._connectStack = [] 488 | }) 489 | } 490 | 491 | get size () { 492 | return this.pool.numFree() + this.pool.numUsed() + this.pool.numPendingCreates() 493 | } 494 | 495 | get available () { 496 | return this.pool.numFree() 497 | } 498 | 499 | get pending () { 500 | return this.pool.numPendingAcquires() 501 | } 502 | 503 | get borrowed () { 504 | return this.pool.numUsed() 505 | } 506 | 507 | /** 508 | * Close all active connections in the pool. 509 | * 510 | * @param {basicCallback} [callback] A callback which is called after connection has closed, or an error has occurred. If omited, method returns Promise. 511 | * @return {ConnectionPool|Promise} 512 | */ 513 | 514 | close (callback) { 515 | if (typeof callback === 'function') { 516 | this._close(callback) 517 | return this 518 | } 519 | 520 | return new shared.Promise((resolve, reject) => { 521 | this._close(err => { 522 | if (err) return reject(err) 523 | resolve(this) 524 | }) 525 | }) 526 | } 527 | 528 | /** 529 | * @private 530 | * @param {basicCallback} callback 531 | */ 532 | 533 | _close (callback) { 534 | // we don't allow pools in a connecting state to be closed because it means there are far too many 535 | // edge cases to deal with 536 | if (this._connecting) { 537 | debug('pool(%d): close called while connecting', IDS.get(this)) 538 | setImmediate(callback, new ConnectionError('Cannot close a pool while it is connecting')) 539 | } 540 | 541 | if (!this.pool) { 542 | debug('pool(%d): already closed, executing close callback immediately', IDS.get(this)) 543 | return setImmediate(callback, null) 544 | } 545 | 546 | this._closeStack.push(callback) 547 | 548 | if (this.pool.destroyed) return 549 | 550 | this._connecting = this._connected = this._healthy = false 551 | 552 | this.pool.destroy().then(() => { 553 | debug('pool(%d): pool closed, removing pool reference and executing close callbacks', IDS.get(this)) 554 | this.pool = null 555 | this._closeStack.forEach(cb => { 556 | setImmediate(cb, null) 557 | }) 558 | }).catch(err => { 559 | this.pool = null 560 | this._closeStack.forEach(cb => { 561 | setImmediate(cb, err) 562 | }) 563 | }).then(() => { 564 | this._closeStack = [] 565 | }) 566 | } 567 | 568 | /** 569 | * Returns new request using this connection. 570 | * 571 | * @return {Request} 572 | */ 573 | 574 | request () { 575 | return new shared.driver.Request(this) 576 | } 577 | 578 | /** 579 | * Returns new transaction using this connection. 580 | * 581 | * @return {Transaction} 582 | */ 583 | 584 | transaction () { 585 | return new shared.driver.Transaction(this) 586 | } 587 | 588 | /** 589 | * Creates a new query using this connection from a tagged template string. 590 | * 591 | * @variation 1 592 | * @param {Array} strings Array of string literals. 593 | * @param {...*} keys Values. 594 | * @return {Request} 595 | */ 596 | 597 | /** 598 | * Execute the SQL command. 599 | * 600 | * @variation 2 601 | * @param {String} command T-SQL command to be executed. 602 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 603 | * @return {Request|Promise} 604 | */ 605 | 606 | query () { 607 | if (typeof arguments[0] === 'string') { return new shared.driver.Request(this).query(arguments[0], arguments[1]) } 608 | 609 | const values = Array.prototype.slice.call(arguments) 610 | const strings = values.shift() 611 | 612 | return new shared.driver.Request(this)._template(strings, values, 'query') 613 | } 614 | 615 | /** 616 | * Creates a new batch using this connection from a tagged template string. 617 | * 618 | * @variation 1 619 | * @param {Array} strings Array of string literals. 620 | * @param {...*} keys Values. 621 | * @return {Request} 622 | */ 623 | 624 | /** 625 | * Execute the SQL command. 626 | * 627 | * @variation 2 628 | * @param {String} command T-SQL command to be executed. 629 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 630 | * @return {Request|Promise} 631 | */ 632 | 633 | batch () { 634 | if (typeof arguments[0] === 'string') { return new shared.driver.Request(this).batch(arguments[0], arguments[1]) } 635 | 636 | const values = Array.prototype.slice.call(arguments) 637 | const strings = values.shift() 638 | 639 | return new shared.driver.Request(this)._template(strings, values, 'batch') 640 | } 641 | } 642 | 643 | module.exports = ConnectionPool 644 | -------------------------------------------------------------------------------- /lib/base/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ConnectionPool = require('./connection-pool') 4 | const PreparedStatement = require('./prepared-statement') 5 | const Request = require('./request') 6 | const Transaction = require('./transaction') 7 | const { ConnectionError, TransactionError, RequestError, PreparedStatementError, MSSQLError } = require('../error') 8 | const shared = require('../shared') 9 | const Table = require('../table') 10 | const ISOLATION_LEVEL = require('../isolationlevel') 11 | const { TYPES } = require('../datatypes') 12 | const { connect, close, on, off, removeListener, query, batch } = require('../global-connection') 13 | 14 | module.exports = { 15 | ConnectionPool, 16 | Transaction, 17 | Request, 18 | PreparedStatement, 19 | ConnectionError, 20 | TransactionError, 21 | RequestError, 22 | PreparedStatementError, 23 | MSSQLError, 24 | driver: shared.driver, 25 | exports: { 26 | ConnectionError, 27 | TransactionError, 28 | RequestError, 29 | PreparedStatementError, 30 | MSSQLError, 31 | Table, 32 | ISOLATION_LEVEL, 33 | TYPES, 34 | MAX: 65535, // (1 << 16) - 1 35 | map: shared.map, 36 | getTypeByValue: shared.getTypeByValue, 37 | connect, 38 | close, 39 | on, 40 | removeListener, 41 | off, 42 | query, 43 | batch 44 | } 45 | } 46 | 47 | Object.defineProperty(module.exports, 'Promise', { 48 | enumerable: true, 49 | get: () => { 50 | return shared.Promise 51 | }, 52 | set: (value) => { 53 | shared.Promise = value 54 | } 55 | }) 56 | 57 | Object.defineProperty(module.exports, 'valueHandler', { 58 | enumerable: true, 59 | value: shared.valueHandler, 60 | writable: false, 61 | configurable: false 62 | }) 63 | 64 | for (const key in TYPES) { 65 | const value = TYPES[key] 66 | module.exports.exports[key] = value 67 | module.exports.exports[key.toUpperCase()] = value 68 | } 69 | 70 | /** 71 | * @callback Request~requestCallback 72 | * @param {Error} err Error on error, otherwise null. 73 | * @param {Object} [result] Request result. 74 | */ 75 | 76 | /** 77 | * @callback Request~bulkCallback 78 | * @param {Error} err Error on error, otherwise null. 79 | * @param {Number} [rowsAffected] Number of affected rows. 80 | */ 81 | 82 | /** 83 | * @callback basicCallback 84 | * @param {Error} err Error on error, otherwise null. 85 | * @param {Connection} [connection] Acquired connection. 86 | */ 87 | 88 | /** 89 | * @callback acquireCallback 90 | * @param {Error} err Error on error, otherwise null. 91 | * @param {Connection} [connection] Acquired connection. 92 | * @param {Object} [config] Connection config 93 | */ 94 | 95 | /** 96 | * Dispatched after connection has established. 97 | * @event ConnectionPool#connect 98 | */ 99 | 100 | /** 101 | * Dispatched after connection has closed a pool (by calling close). 102 | * @event ConnectionPool#close 103 | */ 104 | 105 | /** 106 | * Dispatched when transaction begin. 107 | * @event Transaction#begin 108 | */ 109 | 110 | /** 111 | * Dispatched on successful commit. 112 | * @event Transaction#commit 113 | */ 114 | 115 | /** 116 | * Dispatched on successful rollback. 117 | * @event Transaction#rollback 118 | */ 119 | 120 | /** 121 | * Dispatched when metadata for new recordset are parsed. 122 | * @event Request#recordset 123 | */ 124 | 125 | /** 126 | * Dispatched when new row is parsed. 127 | * @event Request#row 128 | */ 129 | 130 | /** 131 | * Dispatched when request is complete. 132 | * @event Request#done 133 | */ 134 | 135 | /** 136 | * Dispatched on error. 137 | * @event Request#error 138 | */ 139 | -------------------------------------------------------------------------------- /lib/base/prepared-statement.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('mssql:base') 4 | const { EventEmitter } = require('node:events') 5 | const { IDS, objectHasProperty } = require('../utils') 6 | const globalConnection = require('../global-connection') 7 | const { TransactionError, PreparedStatementError } = require('../error') 8 | const shared = require('../shared') 9 | const { TYPES, declare } = require('../datatypes') 10 | 11 | /** 12 | * Class PreparedStatement. 13 | * 14 | * IMPORTANT: Rememeber that each prepared statement means one reserved connection from the pool. Don't forget to unprepare a prepared statement! 15 | * 16 | * @property {String} statement Prepared SQL statement. 17 | */ 18 | 19 | class PreparedStatement extends EventEmitter { 20 | /** 21 | * Creates a new Prepared Statement. 22 | * 23 | * @param {ConnectionPool|Transaction} [holder] 24 | */ 25 | 26 | constructor (parent) { 27 | super() 28 | 29 | IDS.add(this, 'PreparedStatement') 30 | debug('ps(%d): created', IDS.get(this)) 31 | 32 | this.parent = parent || globalConnection.pool 33 | this._handle = 0 34 | this.prepared = false 35 | this.parameters = {} 36 | } 37 | 38 | get config () { 39 | return this.parent.config 40 | } 41 | 42 | get connected () { 43 | return this.parent.connected 44 | } 45 | 46 | /** 47 | * Acquire connection from connection pool. 48 | * 49 | * @param {Request} request Request. 50 | * @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise. 51 | * @return {PreparedStatement|Promise} 52 | */ 53 | 54 | acquire (request, callback) { 55 | if (!this._acquiredConnection) { 56 | setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED')) 57 | return this 58 | } 59 | 60 | if (this._activeRequest) { 61 | setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG')) 62 | return this 63 | } 64 | 65 | this._activeRequest = request 66 | setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig) 67 | return this 68 | } 69 | 70 | /** 71 | * Release connection back to the pool. 72 | * 73 | * @param {Connection} connection Previously acquired connection. 74 | * @return {PreparedStatement} 75 | */ 76 | 77 | release (connection) { 78 | if (connection === this._acquiredConnection) { 79 | this._activeRequest = null 80 | } 81 | 82 | return this 83 | } 84 | 85 | /** 86 | * Add an input parameter to the prepared statement. 87 | * 88 | * @param {String} name Name of the input parameter without @ char. 89 | * @param {*} type SQL data type of input parameter. 90 | * @return {PreparedStatement} 91 | */ 92 | 93 | input (name, type) { 94 | if (/--| |\/\*|\*\/|'/.test(name)) { 95 | throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT') 96 | } 97 | 98 | if (arguments.length < 2) { 99 | throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS') 100 | } 101 | 102 | if (type instanceof Function) { 103 | type = type() 104 | } 105 | 106 | if (objectHasProperty(this.parameters, name)) { 107 | throw new PreparedStatementError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM') 108 | } 109 | 110 | this.parameters[name] = { 111 | name, 112 | type: type.type, 113 | io: 1, 114 | length: type.length, 115 | scale: type.scale, 116 | precision: type.precision, 117 | tvpType: type.tvpType 118 | } 119 | 120 | return this 121 | } 122 | 123 | /** 124 | * Replace an input parameter on the request. 125 | * 126 | * @param {String} name Name of the input parameter without @ char. 127 | * @param {*} [type] SQL data type of input parameter. If you omit type, module automaticaly decide which SQL data type should be used based on JS data type. 128 | * @param {*} value Input parameter value. `undefined` and `NaN` values are automatically converted to `null` values. 129 | * @return {Request} 130 | */ 131 | 132 | replaceInput (name, type, value) { 133 | delete this.parameters[name] 134 | 135 | return this.input(name, type, value) 136 | } 137 | 138 | /** 139 | * Add an output parameter to the prepared statement. 140 | * 141 | * @param {String} name Name of the output parameter without @ char. 142 | * @param {*} type SQL data type of output parameter. 143 | * @return {PreparedStatement} 144 | */ 145 | 146 | output (name, type) { 147 | if (/--| |\/\*|\*\/|'/.test(name)) { 148 | throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT') 149 | } 150 | 151 | if (arguments.length < 2) { 152 | throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS') 153 | } 154 | 155 | if (type instanceof Function) type = type() 156 | 157 | if (objectHasProperty(this.parameters, name)) { 158 | throw new PreparedStatementError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM') 159 | } 160 | 161 | this.parameters[name] = { 162 | name, 163 | type: type.type, 164 | io: 2, 165 | length: type.length, 166 | scale: type.scale, 167 | precision: type.precision 168 | } 169 | 170 | return this 171 | } 172 | 173 | /** 174 | * Replace an output parameter on the request. 175 | * 176 | * @param {String} name Name of the output parameter without @ char. 177 | * @param {*} type SQL data type of output parameter. 178 | * @return {PreparedStatement} 179 | */ 180 | 181 | replaceOutput (name, type) { 182 | delete this.parameters[name] 183 | 184 | return this.output(name, type) 185 | } 186 | 187 | /** 188 | * Prepare a statement. 189 | * 190 | * @param {String} statement SQL statement to prepare. 191 | * @param {basicCallback} [callback] A callback which is called after preparation has completed, or an error has occurred. If omited, method returns Promise. 192 | * @return {PreparedStatement|Promise} 193 | */ 194 | 195 | prepare (statement, callback) { 196 | if (typeof callback === 'function') { 197 | this._prepare(statement, callback) 198 | return this 199 | } 200 | 201 | return new shared.Promise((resolve, reject) => { 202 | this._prepare(statement, err => { 203 | if (err) return reject(err) 204 | resolve(this) 205 | }) 206 | }) 207 | } 208 | 209 | /** 210 | * @private 211 | * @param {String} statement 212 | * @param {basicCallback} callback 213 | */ 214 | 215 | _prepare (statement, callback) { 216 | debug('ps(%d): prepare', IDS.get(this)) 217 | 218 | if (typeof statement === 'function') { 219 | callback = statement 220 | statement = undefined 221 | } 222 | 223 | if (this.prepared) { 224 | return setImmediate(callback, new PreparedStatementError('Statement is already prepared.', 'EALREADYPREPARED')) 225 | } 226 | 227 | this.statement = statement || this.statement 228 | 229 | this.parent.acquire(this, (err, connection, config) => { 230 | if (err) return callback(err) 231 | 232 | this._acquiredConnection = connection 233 | this._acquiredConfig = config 234 | 235 | const req = new shared.driver.Request(this) 236 | req.stream = false 237 | req.output('handle', TYPES.Int) 238 | req.input('params', TYPES.NVarChar, ((() => { 239 | const result = [] 240 | for (const name in this.parameters) { 241 | if (!objectHasProperty(this.parameters, name)) { 242 | continue 243 | } 244 | const param = this.parameters[name] 245 | result.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`) 246 | } 247 | return result 248 | })()).join(',')) 249 | req.input('stmt', TYPES.NVarChar, this.statement) 250 | req.execute('sp_prepare', (err, result) => { 251 | if (err) { 252 | this.parent.release(this._acquiredConnection) 253 | this._acquiredConnection = null 254 | this._acquiredConfig = null 255 | 256 | return callback(err) 257 | } 258 | 259 | debug('ps(%d): prepared', IDS.get(this)) 260 | 261 | this._handle = result.output.handle 262 | this.prepared = true 263 | 264 | callback(null) 265 | }) 266 | }) 267 | } 268 | 269 | /** 270 | * Execute a prepared statement. 271 | * 272 | * @param {Object} values An object whose names correspond to the names of parameters that were added to the prepared statement before it was prepared. 273 | * @param {basicCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 274 | * @return {Request|Promise} 275 | */ 276 | 277 | execute (values, callback) { 278 | if (this.stream || (typeof callback === 'function')) { 279 | return this._execute(values, callback) 280 | } 281 | 282 | return new shared.Promise((resolve, reject) => { 283 | this._execute(values, (err, recordset) => { 284 | if (err) return reject(err) 285 | resolve(recordset) 286 | }) 287 | }) 288 | } 289 | 290 | /** 291 | * @private 292 | * @param {Object} values 293 | * @param {basicCallback} callback 294 | */ 295 | 296 | _execute (values, callback) { 297 | const req = new shared.driver.Request(this) 298 | req.stream = this.stream 299 | req.arrayRowMode = this.arrayRowMode 300 | req.input('handle', TYPES.Int, this._handle) 301 | 302 | // copy parameters with new values 303 | for (const name in this.parameters) { 304 | if (!objectHasProperty(this.parameters, name)) { 305 | continue 306 | } 307 | const param = this.parameters[name] 308 | req.parameters[name] = { 309 | name, 310 | type: param.type, 311 | io: param.io, 312 | value: values[name], 313 | length: param.length, 314 | scale: param.scale, 315 | precision: param.precision 316 | } 317 | } 318 | 319 | req.execute('sp_execute', (err, result) => { 320 | if (err) return callback(err) 321 | 322 | callback(null, result) 323 | }) 324 | 325 | return req 326 | } 327 | 328 | /** 329 | * Unprepare a prepared statement. 330 | * 331 | * @param {basicCallback} [callback] A callback which is called after unpreparation has completed, or an error has occurred. If omited, method returns Promise. 332 | * @return {PreparedStatement|Promise} 333 | */ 334 | 335 | unprepare (callback) { 336 | if (typeof callback === 'function') { 337 | this._unprepare(callback) 338 | return this 339 | } 340 | 341 | return new shared.Promise((resolve, reject) => { 342 | this._unprepare(err => { 343 | if (err) return reject(err) 344 | resolve() 345 | }) 346 | }) 347 | } 348 | 349 | /** 350 | * @private 351 | * @param {basicCallback} callback 352 | */ 353 | 354 | _unprepare (callback) { 355 | debug('ps(%d): unprepare', IDS.get(this)) 356 | 357 | if (!this.prepared) { 358 | return setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED')) 359 | } 360 | 361 | if (this._activeRequest) { 362 | return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG')) 363 | } 364 | 365 | const req = new shared.driver.Request(this) 366 | req.stream = false 367 | req.input('handle', TYPES.Int, this._handle) 368 | req.execute('sp_unprepare', err => { 369 | if (err) return callback(err) 370 | 371 | this.parent.release(this._acquiredConnection) 372 | this._acquiredConnection = null 373 | this._acquiredConfig = null 374 | this._handle = 0 375 | this.prepared = false 376 | 377 | debug('ps(%d): unprepared', IDS.get(this)) 378 | 379 | return callback(null) 380 | }) 381 | } 382 | } 383 | 384 | module.exports = PreparedStatement 385 | -------------------------------------------------------------------------------- /lib/base/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('mssql:base') 4 | const { EventEmitter } = require('node:events') 5 | const { Readable } = require('node:stream') 6 | const { IDS, objectHasProperty } = require('../utils') 7 | const globalConnection = require('../global-connection') 8 | const { RequestError, ConnectionError } = require('../error') 9 | const { TYPES } = require('../datatypes') 10 | const shared = require('../shared') 11 | 12 | /** 13 | * Class Request. 14 | * 15 | * @property {Transaction} transaction Reference to transaction when request was created in transaction. 16 | * @property {*} parameters Collection of input and output parameters. 17 | * @property {Boolean} canceled `true` if request was canceled. 18 | * 19 | * @fires Request#recordset 20 | * @fires Request#row 21 | * @fires Request#done 22 | * @fires Request#error 23 | */ 24 | 25 | class Request extends EventEmitter { 26 | /** 27 | * Create new Request. 28 | * 29 | * @param {Connection|ConnectionPool|Transaction|PreparedStatement} parent If omitted, global connection is used instead. 30 | */ 31 | 32 | constructor (parent) { 33 | super() 34 | 35 | IDS.add(this, 'Request') 36 | debug('request(%d): created', IDS.get(this)) 37 | 38 | this.canceled = false 39 | this._paused = false 40 | this.parent = parent || globalConnection.pool 41 | this.parameters = {} 42 | this.stream = null 43 | this.arrayRowMode = null 44 | } 45 | 46 | get paused () { 47 | return this._paused 48 | } 49 | 50 | /** 51 | * Generate sql string and set input parameters from tagged template string. 52 | * 53 | * @param {Template literal} template 54 | * @return {String} 55 | */ 56 | 57 | template () { 58 | const values = Array.prototype.slice.call(arguments) 59 | const strings = values.shift() 60 | return this._template(strings, values) 61 | } 62 | 63 | /** 64 | * Fetch request from tagged template string. 65 | * 66 | * @private 67 | * @param {Array} strings 68 | * @param {Array} values 69 | * @param {String} [method] If provided, method is automatically called with serialized command on this object. 70 | * @return {Request} 71 | */ 72 | 73 | _template (strings, values, method) { 74 | const command = [strings[0]] 75 | 76 | for (let index = 0; index < values.length; index++) { 77 | const value = values[index] 78 | // if value is an array, prepare each items as it's own comma separated parameter 79 | if (Array.isArray(value)) { 80 | for (let parameterIndex = 0; parameterIndex < value.length; parameterIndex++) { 81 | this.input(`param${index + 1}_${parameterIndex}`, value[parameterIndex]) 82 | command.push(`@param${index + 1}_${parameterIndex}`) 83 | if (parameterIndex < value.length - 1) { 84 | command.push(', ') 85 | } 86 | } 87 | command.push(strings[index + 1]) 88 | } else { 89 | this.input(`param${index + 1}`, value) 90 | command.push(`@param${index + 1}`, strings[index + 1]) 91 | } 92 | } 93 | 94 | if (method) { 95 | return this[method](command.join('')) 96 | } else { 97 | return command.join('') 98 | } 99 | } 100 | 101 | /** 102 | * Add an input parameter to the request. 103 | * 104 | * @param {String} name Name of the input parameter without @ char. 105 | * @param {*} [type] SQL data type of input parameter. If you omit type, module automatically decides which SQL data type should be used based on JS data type. 106 | * @param {*} value Input parameter value. `undefined` and `NaN` values are automatically converted to `null` values. 107 | * @return {Request} 108 | */ 109 | 110 | input (name, type, value) { 111 | if (/--| |\/\*|\*\/|'/.test(name)) { 112 | throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT') 113 | } 114 | 115 | if (arguments.length < 2) { 116 | throw new RequestError('Invalid number of arguments. At least 2 arguments expected.', 'EARGS') 117 | } else if (arguments.length === 2) { 118 | value = type 119 | type = shared.getTypeByValue(value) 120 | } 121 | 122 | // support for custom data types 123 | if (value && typeof value.valueOf === 'function' && !(value instanceof Date)) value = value.valueOf() 124 | 125 | if (value === undefined) value = null // undefined to null 126 | if (typeof value === 'number' && isNaN(value)) value = null // NaN to null 127 | if (type instanceof Function) type = type() 128 | 129 | if (objectHasProperty(this.parameters, name)) { 130 | throw new RequestError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM') 131 | } 132 | 133 | this.parameters[name] = { 134 | name, 135 | type: type.type, 136 | io: 1, 137 | value, 138 | length: type.length, 139 | scale: type.scale, 140 | precision: type.precision, 141 | tvpType: type.tvpType 142 | } 143 | 144 | return this 145 | } 146 | 147 | /** 148 | * Replace an input parameter on the request. 149 | * 150 | * @param {String} name Name of the input parameter without @ char. 151 | * @param {*} [type] SQL data type of input parameter. If you omit type, module automatically decides which SQL data type should be used based on JS data type. 152 | * @param {*} value Input parameter value. `undefined` and `NaN` values are automatically converted to `null` values. 153 | * @return {Request} 154 | */ 155 | 156 | replaceInput (name, type, value) { 157 | delete this.parameters[name] 158 | 159 | return this.input(name, type, value) 160 | } 161 | 162 | /** 163 | * Add an output parameter to the request. 164 | * 165 | * @param {String} name Name of the output parameter without @ char. 166 | * @param {*} type SQL data type of output parameter. 167 | * @param {*} [value] Output parameter value initial value. `undefined` and `NaN` values are automatically converted to `null` values. Optional. 168 | * @return {Request} 169 | */ 170 | 171 | output (name, type, value) { 172 | if (!type) { type = TYPES.NVarChar } 173 | 174 | if (/--| |\/\*|\*\/|'/.test(name)) { 175 | throw new RequestError(`SQL injection warning for param '${name}'`, 'EINJECT') 176 | } 177 | 178 | if ((type === TYPES.Text) || (type === TYPES.NText) || (type === TYPES.Image)) { 179 | throw new RequestError('Deprecated types (Text, NText, Image) are not supported as OUTPUT parameters.', 'EDEPRECATED') 180 | } 181 | 182 | // support for custom data types 183 | if (value && typeof value.valueOf === 'function' && !(value instanceof Date)) value = value.valueOf() 184 | 185 | if (value === undefined) value = null // undefined to null 186 | if (typeof value === 'number' && isNaN(value)) value = null // NaN to null 187 | if (type instanceof Function) type = type() 188 | 189 | if (objectHasProperty(this.parameters, name)) { 190 | throw new RequestError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM') 191 | } 192 | 193 | this.parameters[name] = { 194 | name, 195 | type: type.type, 196 | io: 2, 197 | value, 198 | length: type.length, 199 | scale: type.scale, 200 | precision: type.precision 201 | } 202 | 203 | return this 204 | } 205 | 206 | /** 207 | * Replace an output parameter on the request. 208 | * 209 | * @param {String} name Name of the output parameter without @ char. 210 | * @param {*} type SQL data type of output parameter. 211 | * @param {*} [value] Output parameter value initial value. `undefined` and `NaN` values are automatically converted to `null` values. Optional. 212 | * @return {Request} 213 | */ 214 | 215 | replaceOutput (name, type, value) { 216 | delete this.parameters[name] 217 | 218 | return this.output(name, type, value) 219 | } 220 | 221 | /** 222 | * Execute the SQL batch. 223 | * 224 | * @param {String} batch T-SQL batch to be executed. 225 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 226 | * @return {Request|Promise} 227 | */ 228 | 229 | batch (batch, callback) { 230 | if (this.stream === null && this.parent) this.stream = this.parent.config.stream 231 | if (this.arrayRowMode === null && this.parent) this.arrayRowMode = this.parent.config.arrayRowMode 232 | this.rowsAffected = 0 233 | 234 | if (typeof callback === 'function') { 235 | this._batch(batch, (err, recordsets, output, rowsAffected) => { 236 | if (this.stream) { 237 | if (err) this.emit('error', err) 238 | err = null 239 | 240 | this.emit('done', { 241 | output, 242 | rowsAffected 243 | }) 244 | } 245 | 246 | if (err) return callback(err) 247 | callback(null, { 248 | recordsets, 249 | recordset: recordsets && recordsets[0], 250 | output, 251 | rowsAffected 252 | }) 253 | }) 254 | return this 255 | } 256 | 257 | // Check if method was called as tagged template 258 | if (typeof batch === 'object') { 259 | const values = Array.prototype.slice.call(arguments) 260 | const strings = values.shift() 261 | batch = this._template(strings, values) 262 | } 263 | 264 | return new shared.Promise((resolve, reject) => { 265 | this._batch(batch, (err, recordsets, output, rowsAffected) => { 266 | if (this.stream) { 267 | if (err) this.emit('error', err) 268 | err = null 269 | 270 | this.emit('done', { 271 | output, 272 | rowsAffected 273 | }) 274 | } 275 | 276 | if (err) return reject(err) 277 | resolve({ 278 | recordsets, 279 | recordset: recordsets && recordsets[0], 280 | output, 281 | rowsAffected 282 | }) 283 | }) 284 | }) 285 | } 286 | 287 | /** 288 | * @private 289 | * @param {String} batch 290 | * @param {Request~requestCallback} callback 291 | */ 292 | 293 | _batch (batch, callback) { 294 | if (!this.parent) { 295 | return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN')) 296 | } 297 | 298 | if (!this.parent.connected) { 299 | return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED')) 300 | } 301 | 302 | this.canceled = false 303 | setImmediate(callback) 304 | } 305 | 306 | /** 307 | * Bulk load. 308 | * 309 | * @param {Table} table SQL table. 310 | * @param {object} [options] Options to be passed to the underlying driver (tedious only). 311 | * @param {Request~bulkCallback} [callback] A callback which is called after bulk load has completed, or an error has occurred. If omited, method returns Promise. 312 | * @return {Request|Promise} 313 | */ 314 | 315 | bulk (table, options, callback) { 316 | if (typeof options === 'function') { 317 | callback = options 318 | options = {} 319 | } else if (typeof options === 'undefined') { 320 | options = {} 321 | } 322 | 323 | if (this.stream === null && this.parent) this.stream = this.parent.config.stream 324 | if (this.arrayRowMode === null && this.parent) this.arrayRowMode = this.parent.config.arrayRowMode 325 | 326 | if (this.stream || typeof callback === 'function') { 327 | this._bulk(table, options, (err, rowsAffected) => { 328 | if (this.stream) { 329 | if (err) this.emit('error', err) 330 | return this.emit('done', { 331 | rowsAffected 332 | }) 333 | } 334 | 335 | if (err) return callback(err) 336 | callback(null, { 337 | rowsAffected 338 | }) 339 | }) 340 | return this 341 | } 342 | 343 | return new shared.Promise((resolve, reject) => { 344 | this._bulk(table, options, (err, rowsAffected) => { 345 | if (err) return reject(err) 346 | resolve({ 347 | rowsAffected 348 | }) 349 | }) 350 | }) 351 | } 352 | 353 | /** 354 | * @private 355 | * @param {Table} table 356 | * @param {object} options 357 | * @param {Request~bulkCallback} callback 358 | */ 359 | 360 | _bulk (table, options, callback) { 361 | if (!this.parent) { 362 | return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN')) 363 | } 364 | 365 | if (!this.parent.connected) { 366 | return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED')) 367 | } 368 | 369 | this.canceled = false 370 | setImmediate(callback) 371 | } 372 | 373 | /** 374 | * Wrap original request in a Readable stream that supports back pressure and return. 375 | * It also sets request to `stream` mode and pulls all rows from all recordsets to a given stream. 376 | * 377 | * @param {Object} streamOptions - optional options to configure the readable stream with like highWaterMark 378 | * @return {Stream} 379 | */ 380 | 381 | toReadableStream (streamOptions = {}) { 382 | this.stream = true 383 | this.pause() 384 | const readableStream = new Readable({ 385 | ...streamOptions, 386 | objectMode: true, 387 | read: (/* size */) => { 388 | this.resume() 389 | } 390 | }) 391 | this.on('row', (row) => { 392 | if (!readableStream.push(row)) { 393 | this.pause() 394 | } 395 | }) 396 | this.on('error', (error) => { 397 | readableStream.emit('error', error) 398 | }) 399 | this.on('done', () => { 400 | readableStream.push(null) 401 | }) 402 | return readableStream 403 | } 404 | 405 | /** 406 | * Wrap original request in a Readable stream that supports back pressure and pipe to the Writable stream. 407 | * It also sets request to `stream` mode and pulls all rows from all recordsets to a given stream. 408 | * 409 | * @param {Stream} stream Stream to pipe data into. 410 | * @return {Stream} 411 | */ 412 | 413 | pipe (writableStream) { 414 | const readableStream = this.toReadableStream() 415 | return readableStream.pipe(writableStream) 416 | } 417 | 418 | /** 419 | * Execute the SQL command. 420 | * 421 | * @param {String} command T-SQL command to be executed. 422 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 423 | * @return {Request|Promise} 424 | */ 425 | 426 | query (command, callback) { 427 | if (this.stream === null && this.parent) this.stream = this.parent.config.stream 428 | if (this.arrayRowMode === null && this.parent) this.arrayRowMode = this.parent.config.arrayRowMode 429 | this.rowsAffected = 0 430 | 431 | if (typeof callback === 'function') { 432 | this._query(command, (err, recordsets, output, rowsAffected, columns) => { 433 | if (this.stream) { 434 | if (err) this.emit('error', err) 435 | err = null 436 | 437 | this.emit('done', { 438 | output, 439 | rowsAffected 440 | }) 441 | } 442 | 443 | if (err) return callback(err) 444 | const result = { 445 | recordsets, 446 | recordset: recordsets && recordsets[0], 447 | output, 448 | rowsAffected 449 | } 450 | if (this.arrayRowMode) result.columns = columns 451 | callback(null, result) 452 | }) 453 | return this 454 | } 455 | 456 | // Check if method was called as tagged template 457 | if (typeof command === 'object') { 458 | const values = Array.prototype.slice.call(arguments) 459 | const strings = values.shift() 460 | command = this._template(strings, values) 461 | } 462 | 463 | return new shared.Promise((resolve, reject) => { 464 | this._query(command, (err, recordsets, output, rowsAffected, columns) => { 465 | if (this.stream) { 466 | if (err) this.emit('error', err) 467 | err = null 468 | 469 | this.emit('done', { 470 | output, 471 | rowsAffected 472 | }) 473 | } 474 | 475 | if (err) return reject(err) 476 | const result = { 477 | recordsets, 478 | recordset: recordsets && recordsets[0], 479 | output, 480 | rowsAffected 481 | } 482 | if (this.arrayRowMode) result.columns = columns 483 | resolve(result) 484 | }) 485 | }) 486 | } 487 | 488 | /** 489 | * @private 490 | * @param {String} command 491 | * @param {Request~bulkCallback} callback 492 | */ 493 | 494 | _query (command, callback) { 495 | if (!this.parent) { 496 | return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN')) 497 | } 498 | 499 | if (!this.parent.connected) { 500 | return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED')) 501 | } 502 | 503 | this.canceled = false 504 | setImmediate(callback) 505 | } 506 | 507 | /** 508 | * Call a stored procedure. 509 | * 510 | * @param {String} procedure Name of the stored procedure to be executed. 511 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 512 | * @return {Request|Promise} 513 | */ 514 | 515 | execute (command, callback) { 516 | if (this.stream === null && this.parent) this.stream = this.parent.config.stream 517 | if (this.arrayRowMode === null && this.parent) this.arrayRowMode = this.parent.config.arrayRowMode 518 | this.rowsAffected = 0 519 | 520 | if (typeof callback === 'function') { 521 | this._execute(command, (err, recordsets, output, returnValue, rowsAffected, columns) => { 522 | if (this.stream) { 523 | if (err) this.emit('error', err) 524 | err = null 525 | 526 | this.emit('done', { 527 | output, 528 | rowsAffected, 529 | returnValue 530 | }) 531 | } 532 | 533 | if (err) return callback(err) 534 | const result = { 535 | recordsets, 536 | recordset: recordsets && recordsets[0], 537 | output, 538 | rowsAffected, 539 | returnValue 540 | } 541 | if (this.arrayRowMode) result.columns = columns 542 | callback(null, result) 543 | }) 544 | return this 545 | } 546 | 547 | return new shared.Promise((resolve, reject) => { 548 | this._execute(command, (err, recordsets, output, returnValue, rowsAffected, columns) => { 549 | if (this.stream) { 550 | if (err) this.emit('error', err) 551 | err = null 552 | 553 | this.emit('done', { 554 | output, 555 | rowsAffected, 556 | returnValue 557 | }) 558 | } 559 | 560 | if (err) return reject(err) 561 | const result = { 562 | recordsets, 563 | recordset: recordsets && recordsets[0], 564 | output, 565 | rowsAffected, 566 | returnValue 567 | } 568 | if (this.arrayRowMode) result.columns = columns 569 | resolve(result) 570 | }) 571 | }) 572 | } 573 | 574 | /** 575 | * @private 576 | * @param {String} procedure 577 | * @param {Request~bulkCallback} callback 578 | */ 579 | 580 | _execute (procedure, callback) { 581 | if (!this.parent) { 582 | return setImmediate(callback, new RequestError('No connection is specified for that request.', 'ENOCONN')) 583 | } 584 | 585 | if (!this.parent.connected) { 586 | return setImmediate(callback, new ConnectionError('Connection is closed.', 'ECONNCLOSED')) 587 | } 588 | 589 | this.canceled = false 590 | setImmediate(callback) 591 | } 592 | 593 | /** 594 | * Cancel currently executed request. 595 | * 596 | * @return {Boolean} 597 | */ 598 | 599 | cancel () { 600 | this._cancel() 601 | return true 602 | } 603 | 604 | /** 605 | * @private 606 | */ 607 | 608 | _cancel () { 609 | this.canceled = true 610 | } 611 | 612 | pause () { 613 | if (this.stream) { 614 | this._pause() 615 | return true 616 | } 617 | return false 618 | } 619 | 620 | _pause () { 621 | this._paused = true 622 | } 623 | 624 | resume () { 625 | if (this.stream) { 626 | this._resume() 627 | return true 628 | } 629 | return false 630 | } 631 | 632 | _resume () { 633 | this._paused = false 634 | } 635 | 636 | _setCurrentRequest (request) { 637 | this._currentRequest = request 638 | if (this._paused) { 639 | this.pause() 640 | } 641 | return this 642 | } 643 | } 644 | 645 | module.exports = Request 646 | -------------------------------------------------------------------------------- /lib/base/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('mssql:base') 4 | const { EventEmitter } = require('node:events') 5 | const { IDS } = require('../utils') 6 | const globalConnection = require('../global-connection') 7 | const { TransactionError } = require('../error') 8 | const shared = require('../shared') 9 | const ISOLATION_LEVEL = require('../isolationlevel') 10 | 11 | /** 12 | * Class Transaction. 13 | * 14 | * @property {Number} isolationLevel Controls the locking and row versioning behavior of TSQL statements issued by a connection. READ_COMMITTED by default. 15 | * @property {String} name Transaction name. Empty string by default. 16 | * 17 | * @fires Transaction#begin 18 | * @fires Transaction#commit 19 | * @fires Transaction#rollback 20 | */ 21 | 22 | class Transaction extends EventEmitter { 23 | /** 24 | * Create new Transaction. 25 | * 26 | * @param {Connection} [parent] If ommited, global connection is used instead. 27 | */ 28 | 29 | constructor (parent) { 30 | super() 31 | 32 | IDS.add(this, 'Transaction') 33 | debug('transaction(%d): created', IDS.get(this)) 34 | 35 | this.parent = parent || globalConnection.pool 36 | this.isolationLevel = Transaction.defaultIsolationLevel 37 | this.name = '' 38 | } 39 | 40 | get config () { 41 | return this.parent.config 42 | } 43 | 44 | get connected () { 45 | return this.parent.connected 46 | } 47 | 48 | /** 49 | * Acquire connection from connection pool. 50 | * 51 | * @param {Request} request Request. 52 | * @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise. 53 | * @return {Transaction|Promise} 54 | */ 55 | 56 | acquire (request, callback) { 57 | if (!this._acquiredConnection) { 58 | setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN')) 59 | return this 60 | } 61 | 62 | if (this._activeRequest) { 63 | setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG')) 64 | return this 65 | } 66 | 67 | this._activeRequest = request 68 | setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig) 69 | return this 70 | } 71 | 72 | /** 73 | * Release connection back to the pool. 74 | * 75 | * @param {Connection} connection Previously acquired connection. 76 | * @return {Transaction} 77 | */ 78 | 79 | release (connection) { 80 | if (connection === this._acquiredConnection) { 81 | this._activeRequest = null 82 | } 83 | 84 | return this 85 | } 86 | 87 | /** 88 | * Begin a transaction. 89 | * 90 | * @param {Number} [isolationLevel] Controls the locking and row versioning behavior of TSQL statements issued by a connection. 91 | * @param {basicCallback} [callback] A callback which is called after transaction has began, or an error has occurred. If omited, method returns Promise. 92 | * @return {Transaction|Promise} 93 | */ 94 | 95 | begin (isolationLevel, callback) { 96 | if (isolationLevel instanceof Function) { 97 | callback = isolationLevel 98 | isolationLevel = undefined 99 | } 100 | 101 | if (typeof callback === 'function') { 102 | this._begin(isolationLevel, err => { 103 | if (!err) { 104 | this.emit('begin') 105 | } 106 | callback(err) 107 | }) 108 | return this 109 | } 110 | 111 | return new shared.Promise((resolve, reject) => { 112 | this._begin(isolationLevel, err => { 113 | if (err) return reject(err) 114 | this.emit('begin') 115 | resolve(this) 116 | }) 117 | }) 118 | } 119 | 120 | /** 121 | * @private 122 | * @param {Number} [isolationLevel] 123 | * @param {basicCallback} [callback] 124 | * @return {Transaction} 125 | */ 126 | 127 | _begin (isolationLevel, callback) { 128 | if (this._acquiredConnection) { 129 | return setImmediate(callback, new TransactionError('Transaction has already begun.', 'EALREADYBEGUN')) 130 | } 131 | 132 | this._aborted = false 133 | this._rollbackRequested = false 134 | if (isolationLevel) { 135 | if (Object.keys(ISOLATION_LEVEL).some(key => { 136 | return ISOLATION_LEVEL[key] === isolationLevel 137 | })) { 138 | this.isolationLevel = isolationLevel 139 | } else { 140 | throw new TransactionError('Invalid isolation level.') 141 | } 142 | } 143 | 144 | setImmediate(callback) 145 | } 146 | 147 | /** 148 | * Commit a transaction. 149 | * 150 | * @param {basicCallback} [callback] A callback which is called after transaction has commited, or an error has occurred. If omited, method returns Promise. 151 | * @return {Transaction|Promise} 152 | */ 153 | 154 | commit (callback) { 155 | if (typeof callback === 'function') { 156 | this._commit(err => { 157 | if (!err) { 158 | this.emit('commit') 159 | } 160 | callback(err) 161 | }) 162 | return this 163 | } 164 | 165 | return new shared.Promise((resolve, reject) => { 166 | this._commit(err => { 167 | if (err) return reject(err) 168 | this.emit('commit') 169 | resolve() 170 | }) 171 | }) 172 | } 173 | 174 | /** 175 | * @private 176 | * @param {basicCallback} [callback] 177 | * @return {Transaction} 178 | */ 179 | 180 | _commit (callback) { 181 | if (this._aborted) { 182 | return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT')) 183 | } 184 | 185 | if (!this._acquiredConnection) { 186 | return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN')) 187 | } 188 | 189 | if (this._activeRequest) { 190 | return setImmediate(callback, new TransactionError("Can't commit transaction. There is a request in progress.", 'EREQINPROG')) 191 | } 192 | 193 | setImmediate(callback) 194 | } 195 | 196 | /** 197 | * Returns new request using this transaction. 198 | * 199 | * @return {Request} 200 | */ 201 | 202 | request () { 203 | return new shared.driver.Request(this) 204 | } 205 | 206 | /** 207 | * Rollback a transaction. 208 | * 209 | * @param {basicCallback} [callback] A callback which is called after transaction has rolled back, or an error has occurred. If omited, method returns Promise. 210 | * @return {Transaction|Promise} 211 | */ 212 | 213 | rollback (callback) { 214 | if (typeof callback === 'function') { 215 | this._rollback(err => { 216 | if (!err) { 217 | this.emit('rollback', this._aborted) 218 | } 219 | callback(err) 220 | }) 221 | return this 222 | } 223 | 224 | return new shared.Promise((resolve, reject) => { 225 | return this._rollback(err => { 226 | if (err) return reject(err) 227 | this.emit('rollback', this._aborted) 228 | resolve() 229 | }) 230 | }) 231 | } 232 | 233 | /** 234 | * @private 235 | * @param {basicCallback} [callback] 236 | * @return {Transaction} 237 | */ 238 | 239 | _rollback (callback) { 240 | if (this._aborted) { 241 | return setImmediate(callback, new TransactionError('Transaction has been aborted.', 'EABORT')) 242 | } 243 | 244 | if (!this._acquiredConnection) { 245 | return setImmediate(callback, new TransactionError('Transaction has not begun. Call begin() first.', 'ENOTBEGUN')) 246 | } 247 | 248 | if (this._activeRequest) { 249 | return setImmediate(callback, new TransactionError("Can't rollback transaction. There is a request in progress.", 'EREQINPROG')) 250 | } 251 | 252 | this._rollbackRequested = true 253 | 254 | setImmediate(callback) 255 | } 256 | } 257 | 258 | /** 259 | * Default isolation level used for any transactions that don't explicitly specify an isolation level. 260 | * 261 | * @type {number} 262 | */ 263 | Transaction.defaultIsolationLevel = ISOLATION_LEVEL.READ_COMMITTED 264 | 265 | module.exports = Transaction 266 | -------------------------------------------------------------------------------- /lib/datatypes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const objectHasProperty = require('./utils').objectHasProperty 4 | const inspect = Symbol.for('nodejs.util.inspect.custom') 5 | 6 | const TYPES = { 7 | VarChar (length) { 8 | return { type: TYPES.VarChar, length } 9 | }, 10 | NVarChar (length) { 11 | return { type: TYPES.NVarChar, length } 12 | }, 13 | Text () { 14 | return { type: TYPES.Text } 15 | }, 16 | Int () { 17 | return { type: TYPES.Int } 18 | }, 19 | BigInt () { 20 | return { type: TYPES.BigInt } 21 | }, 22 | TinyInt () { 23 | return { type: TYPES.TinyInt } 24 | }, 25 | SmallInt () { 26 | return { type: TYPES.SmallInt } 27 | }, 28 | Bit () { 29 | return { type: TYPES.Bit } 30 | }, 31 | Float () { 32 | return { type: TYPES.Float } 33 | }, 34 | Numeric (precision, scale) { 35 | return { type: TYPES.Numeric, precision, scale } 36 | }, 37 | Decimal (precision, scale) { 38 | return { type: TYPES.Decimal, precision, scale } 39 | }, 40 | Real () { 41 | return { type: TYPES.Real } 42 | }, 43 | Date () { 44 | return { type: TYPES.Date } 45 | }, 46 | DateTime () { 47 | return { type: TYPES.DateTime } 48 | }, 49 | DateTime2 (scale) { 50 | return { type: TYPES.DateTime2, scale } 51 | }, 52 | DateTimeOffset (scale) { 53 | return { type: TYPES.DateTimeOffset, scale } 54 | }, 55 | SmallDateTime () { 56 | return { type: TYPES.SmallDateTime } 57 | }, 58 | Time (scale) { 59 | return { type: TYPES.Time, scale } 60 | }, 61 | UniqueIdentifier () { 62 | return { type: TYPES.UniqueIdentifier } 63 | }, 64 | SmallMoney () { 65 | return { type: TYPES.SmallMoney } 66 | }, 67 | Money () { 68 | return { type: TYPES.Money } 69 | }, 70 | Binary (length) { 71 | return { type: TYPES.Binary, length } 72 | }, 73 | VarBinary (length) { 74 | return { type: TYPES.VarBinary, length } 75 | }, 76 | Image () { 77 | return { type: TYPES.Image } 78 | }, 79 | Xml () { 80 | return { type: TYPES.Xml } 81 | }, 82 | Char (length) { 83 | return { type: TYPES.Char, length } 84 | }, 85 | NChar (length) { 86 | return { type: TYPES.NChar, length } 87 | }, 88 | NText () { 89 | return { type: TYPES.NText } 90 | }, 91 | TVP (tvpType) { 92 | return { type: TYPES.TVP, tvpType } 93 | }, 94 | UDT () { 95 | return { type: TYPES.UDT } 96 | }, 97 | Geography () { 98 | return { type: TYPES.Geography } 99 | }, 100 | Geometry () { 101 | return { type: TYPES.Geometry } 102 | }, 103 | Variant () { 104 | return { type: TYPES.Variant } 105 | } 106 | } 107 | 108 | module.exports.TYPES = TYPES 109 | module.exports.DECLARATIONS = {} 110 | 111 | const zero = function (value, length) { 112 | if (length == null) length = 2 113 | 114 | value = String(value) 115 | if (value.length < length) { 116 | for (let i = 1; i <= length - value.length; i++) { 117 | value = `0${value}` 118 | } 119 | } 120 | return value 121 | } 122 | 123 | for (const key in TYPES) { 124 | if (objectHasProperty(TYPES, key)) { 125 | const value = TYPES[key] 126 | value.declaration = key.toLowerCase() 127 | module.exports.DECLARATIONS[value.declaration] = value; 128 | 129 | ((key, value) => { 130 | value[inspect] = () => `[sql.${key}]` 131 | })(key, value) 132 | } 133 | } 134 | 135 | module.exports.declare = (type, options) => { 136 | switch (type) { 137 | case TYPES.VarChar: case TYPES.VarBinary: 138 | return `${type.declaration} (${options.length > 8000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})` 139 | case TYPES.NVarChar: 140 | return `${type.declaration} (${options.length > 4000 ? 'MAX' : (options.length == null ? 'MAX' : options.length)})` 141 | case TYPES.Char: case TYPES.NChar: case TYPES.Binary: 142 | return `${type.declaration} (${options.length == null ? 1 : options.length})` 143 | case TYPES.Decimal: case TYPES.Numeric: 144 | return `${type.declaration} (${options.precision == null ? 18 : options.precision}, ${options.scale == null ? 0 : options.scale})` 145 | case TYPES.Time: case TYPES.DateTime2: case TYPES.DateTimeOffset: 146 | return `${type.declaration} (${options.scale == null ? 7 : options.scale})` 147 | case TYPES.TVP: 148 | return `${options.tvpType} readonly` 149 | default: 150 | return type.declaration 151 | } 152 | } 153 | 154 | module.exports.cast = (value, type, options) => { 155 | if (value == null) { 156 | return null 157 | } 158 | 159 | switch (typeof value) { 160 | case 'string': 161 | return `N'${value.replace(/'/g, '\'\'')}'` 162 | 163 | case 'number': 164 | case 'bigint': 165 | return value 166 | 167 | case 'boolean': 168 | return value ? 1 : 0 169 | 170 | case 'object': 171 | if (value instanceof Date) { 172 | let ns = value.getUTCMilliseconds() / 1000 173 | if (value.nanosecondDelta != null) { 174 | ns += value.nanosecondDelta 175 | } 176 | const scale = options.scale == null ? 7 : options.scale 177 | 178 | if (scale > 0) { 179 | ns = String(ns).substr(1, scale + 1) 180 | } else { 181 | ns = '' 182 | } 183 | 184 | return `N'${value.getUTCFullYear()}-${zero(value.getUTCMonth() + 1)}-${zero(value.getUTCDate())} ${zero(value.getUTCHours())}:${zero(value.getUTCMinutes())}:${zero(value.getUTCSeconds())}${ns}'` 185 | } else if (Buffer.isBuffer(value)) { 186 | return `0x${value.toString('hex')}` 187 | } 188 | 189 | return null 190 | 191 | default: 192 | return null 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lib/error/connection-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MSSQLError = require('./mssql-error') 4 | 5 | /** 6 | * Class ConnectionError. 7 | */ 8 | 9 | class ConnectionError extends MSSQLError { 10 | /** 11 | * Creates a new ConnectionError. 12 | * 13 | * @param {String} message Error message. 14 | * @param {String} [code] Error code. 15 | */ 16 | 17 | constructor (message, code) { 18 | super(message, code) 19 | 20 | this.name = 'ConnectionError' 21 | } 22 | } 23 | 24 | module.exports = ConnectionError 25 | -------------------------------------------------------------------------------- /lib/error/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ConnectionError = require('./connection-error') 4 | const MSSQLError = require('./mssql-error') 5 | const PreparedStatementError = require('./prepared-statement-error') 6 | const RequestError = require('./request-error') 7 | const TransactionError = require('./transaction-error') 8 | 9 | module.exports = { 10 | ConnectionError, 11 | MSSQLError, 12 | PreparedStatementError, 13 | RequestError, 14 | TransactionError 15 | } 16 | -------------------------------------------------------------------------------- /lib/error/mssql-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class MSSQLError extends Error { 4 | /** 5 | * Creates a new ConnectionError. 6 | * 7 | * @param {String} message Error message. 8 | * @param {String} [code] Error code. 9 | */ 10 | 11 | constructor (message, code) { 12 | if (message instanceof Error) { 13 | super(message.message) 14 | this.code = message.code || code 15 | 16 | Error.captureStackTrace(this, this.constructor) 17 | Object.defineProperty(this, 'originalError', { enumerable: true, value: message }) 18 | } else { 19 | super(message) 20 | this.code = code 21 | } 22 | 23 | this.name = 'MSSQLError' 24 | } 25 | } 26 | 27 | module.exports = MSSQLError 28 | -------------------------------------------------------------------------------- /lib/error/prepared-statement-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MSSQLError = require('./mssql-error') 4 | 5 | /** 6 | * Class PreparedStatementError. 7 | */ 8 | 9 | class PreparedStatementError extends MSSQLError { 10 | /** 11 | * Creates a new PreparedStatementError. 12 | * 13 | * @param {String} message Error message. 14 | * @param {String} [code] Error code. 15 | */ 16 | 17 | constructor (message, code) { 18 | super(message, code) 19 | 20 | this.name = 'PreparedStatementError' 21 | } 22 | } 23 | 24 | module.exports = PreparedStatementError 25 | -------------------------------------------------------------------------------- /lib/error/request-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MSSQLError = require('./mssql-error') 4 | 5 | /** 6 | * Class RequestError. 7 | * 8 | * @property {String} number Error number. 9 | * @property {Number} lineNumber Line number. 10 | * @property {String} state Error state. 11 | * @property {String} class Error class. 12 | * @property {String} serverName Server name. 13 | * @property {String} procName Procedure name. 14 | */ 15 | 16 | class RequestError extends MSSQLError { 17 | /** 18 | * Creates a new RequestError. 19 | * 20 | * @param {String} message Error message. 21 | * @param {String} [code] Error code. 22 | */ 23 | 24 | constructor (message, code) { 25 | super(message, code) 26 | if (message instanceof Error) { 27 | if (message.info) { 28 | this.number = message.info.number || message.code // err.code is returned by msnodesql driver 29 | this.lineNumber = message.info.lineNumber 30 | this.state = message.info.state || message.sqlstate // err.sqlstate is returned by msnodesql driver 31 | this.class = message.info.class 32 | this.serverName = message.info.serverName 33 | this.procName = message.info.procName 34 | } else { 35 | // Use err attributes returned by msnodesql driver 36 | this.number = message.code 37 | this.lineNumber = message.lineNumber 38 | this.state = message.sqlstate 39 | this.class = message.severity 40 | this.serverName = message.serverName 41 | this.procName = message.procName 42 | } 43 | } 44 | 45 | this.name = 'RequestError' 46 | const parsedMessage = (/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec(this.message) 47 | if (parsedMessage) { 48 | this.message = parsedMessage[1] 49 | } 50 | } 51 | } 52 | 53 | module.exports = RequestError 54 | -------------------------------------------------------------------------------- /lib/error/transaction-error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MSSQLError = require('./mssql-error') 4 | 5 | /** 6 | * Class TransactionError. 7 | */ 8 | 9 | class TransactionError extends MSSQLError { 10 | /** 11 | * Creates a new TransactionError. 12 | * 13 | * @param {String} message Error message. 14 | * @param {String} [code] Error code. 15 | */ 16 | 17 | constructor (message, code) { 18 | super(message, code) 19 | 20 | this.name = 'TransactionError' 21 | } 22 | } 23 | 24 | module.exports = TransactionError 25 | -------------------------------------------------------------------------------- /lib/global-connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const shared = require('./shared') 4 | 5 | let globalConnection = null 6 | const globalConnectionHandlers = {} 7 | 8 | /** 9 | * Open global connection pool. 10 | * 11 | * @param {Object|String} config Connection configuration object or connection string. 12 | * @param {basicCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise. 13 | * @return {Promise.} 14 | */ 15 | 16 | function connect (config, callback) { 17 | if (!globalConnection) { 18 | globalConnection = new shared.driver.ConnectionPool(config) 19 | 20 | for (const event in globalConnectionHandlers) { 21 | for (let i = 0, l = globalConnectionHandlers[event].length; i < l; i++) { 22 | globalConnection.on(event, globalConnectionHandlers[event][i]) 23 | } 24 | } 25 | 26 | const ogClose = globalConnection.close 27 | 28 | const globalClose = function (callback) { 29 | // remove event handlers from the global connection 30 | for (const event in globalConnectionHandlers) { 31 | for (let i = 0, l = globalConnectionHandlers[event].length; i < l; i++) { 32 | this.removeListener(event, globalConnectionHandlers[event][i]) 33 | } 34 | } 35 | 36 | // attach error handler to prevent process crash in case of error 37 | this.on('error', err => { 38 | if (globalConnectionHandlers.error) { 39 | for (let i = 0, l = globalConnectionHandlers.error.length; i < l; i++) { 40 | globalConnectionHandlers.error[i].call(this, err) 41 | } 42 | } 43 | }) 44 | 45 | globalConnection = null 46 | return ogClose.call(this, callback) 47 | } 48 | 49 | globalConnection.close = globalClose.bind(globalConnection) 50 | } 51 | if (typeof callback === 'function') { 52 | return globalConnection.connect((err, connection) => { 53 | if (err) { 54 | globalConnection = null 55 | } 56 | callback(err, connection) 57 | }) 58 | } 59 | return globalConnection.connect().catch((err) => { 60 | globalConnection = null 61 | return shared.Promise.reject(err) 62 | }) 63 | } 64 | 65 | /** 66 | * Close all active connections in the global pool. 67 | * 68 | * @param {basicCallback} [callback] A callback which is called after connection has closed, or an error has occurred. If omited, method returns Promise. 69 | * @return {ConnectionPool|Promise} 70 | */ 71 | 72 | function close (callback) { 73 | if (globalConnection) { 74 | const gc = globalConnection 75 | globalConnection = null 76 | return gc.close(callback) 77 | } 78 | 79 | if (typeof callback === 'function') { 80 | setImmediate(callback) 81 | return null 82 | } 83 | 84 | return new shared.Promise((resolve) => { 85 | resolve(globalConnection) 86 | }) 87 | } 88 | 89 | /** 90 | * Attach event handler to global connection pool. 91 | * 92 | * @param {String} event Event name. 93 | * @param {Function} handler Event handler. 94 | * @return {ConnectionPool} 95 | */ 96 | 97 | function on (event, handler) { 98 | if (!globalConnectionHandlers[event]) globalConnectionHandlers[event] = [] 99 | globalConnectionHandlers[event].push(handler) 100 | 101 | if (globalConnection) globalConnection.on(event, handler) 102 | return globalConnection 103 | } 104 | 105 | /** 106 | * Detach event handler from global connection. 107 | * 108 | * @param {String} event Event name. 109 | * @param {Function} handler Event handler. 110 | * @return {ConnectionPool} 111 | */ 112 | 113 | function removeListener (event, handler) { 114 | if (!globalConnectionHandlers[event]) return globalConnection 115 | const index = globalConnectionHandlers[event].indexOf(handler) 116 | if (index === -1) return globalConnection 117 | globalConnectionHandlers[event].splice(index, 1) 118 | if (globalConnectionHandlers[event].length === 0) globalConnectionHandlers[event] = undefined 119 | 120 | if (globalConnection) globalConnection.removeListener(event, handler) 121 | return globalConnection 122 | } 123 | 124 | /** 125 | * Creates a new query using global connection from a tagged template string. 126 | * 127 | * @variation 1 128 | * @param {Array|String} strings Array of string literals or sql command. 129 | * @param {...*} keys Values. 130 | * @return {Request} 131 | */ 132 | 133 | /** 134 | * Execute the SQL command. 135 | * 136 | * @variation 2 137 | * @param {String} command T-SQL command to be executed. 138 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 139 | * @return {Request|Promise} 140 | */ 141 | 142 | function query () { 143 | if (typeof arguments[0] === 'string') { return new shared.driver.Request().query(arguments[0], arguments[1]) } 144 | 145 | const values = Array.prototype.slice.call(arguments) 146 | const strings = values.shift() 147 | 148 | return new shared.driver.Request()._template(strings, values, 'query') 149 | } 150 | 151 | /** 152 | * Creates a new batch using global connection from a tagged template string. 153 | * 154 | * @variation 1 155 | * @param {Array} strings Array of string literals. 156 | * @param {...*} keys Values. 157 | * @return {Request} 158 | */ 159 | 160 | /** 161 | * Execute the SQL command. 162 | * 163 | * @variation 2 164 | * @param {String} command T-SQL command to be executed. 165 | * @param {Request~requestCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise. 166 | * @return {Request|Promise} 167 | */ 168 | 169 | function batch () { 170 | if (typeof arguments[0] === 'string') { return new shared.driver.Request().batch(arguments[0], arguments[1]) } 171 | 172 | const values = Array.prototype.slice.call(arguments) 173 | const strings = values.shift() 174 | 175 | return new shared.driver.Request()._template(strings, values, 'batch') 176 | } 177 | 178 | module.exports = { 179 | batch, 180 | close, 181 | connect, 182 | off: removeListener, 183 | on, 184 | query, 185 | removeListener 186 | } 187 | 188 | Object.defineProperty(module.exports, 'pool', { 189 | get: () => { 190 | return globalConnection 191 | }, 192 | set: () => {} 193 | }) 194 | -------------------------------------------------------------------------------- /lib/isolationlevel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | READ_UNCOMMITTED: 0x01, 5 | READ_COMMITTED: 0x02, 6 | REPEATABLE_READ: 0x03, 7 | SERIALIZABLE: 0x04, 8 | SNAPSHOT: 0x05 9 | } 10 | -------------------------------------------------------------------------------- /lib/msnodesqlv8/connection-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const msnodesql = require('msnodesqlv8') 4 | const debug = require('debug')('mssql:msv8') 5 | const BaseConnectionPool = require('../base/connection-pool') 6 | const { IDS, INCREMENT } = require('../utils') 7 | const shared = require('../shared') 8 | const ConnectionError = require('../error/connection-error') 9 | const { platform } = require('node:os') 10 | const { buildConnectionString } = require('@tediousjs/connection-string') 11 | 12 | const CONNECTION_DRIVER = ['darwin', 'linux'].includes(platform()) ? 'ODBC Driver 17 for SQL Server' : 'SQL Server Native Client 11.0' 13 | 14 | class ConnectionPool extends BaseConnectionPool { 15 | _poolCreate () { 16 | return new shared.Promise((resolve, reject) => { 17 | this.config.requestTimeout = this.config.requestTimeout ?? this.config.timeout ?? 15000 18 | 19 | const cfg = { 20 | conn_str: this.config.connectionString, 21 | conn_timeout: (this.config.connectionTimeout ?? this.config.timeout ?? 15000) / 1000 22 | } 23 | 24 | if (!this.config.connectionString) { 25 | cfg.conn_str = buildConnectionString({ 26 | Driver: CONNECTION_DRIVER, 27 | Server: this.config.options.instanceName ? `${this.config.server}\\${this.config.options.instanceName}` : `${this.config.server},${this.config.port}`, 28 | Database: this.config.database, 29 | Uid: this.config.user, 30 | Pwd: this.config.password, 31 | Trusted_Connection: !!this.config.options.trustedConnection, 32 | Encrypt: !!this.config.options.encrypt 33 | }) 34 | } 35 | 36 | const connedtionId = INCREMENT.Connection++ 37 | debug('pool(%d): connection #%d created', IDS.get(this), connedtionId) 38 | debug('connection(%d): establishing', connedtionId) 39 | 40 | if (typeof this.config.beforeConnect === 'function') { 41 | this.config.beforeConnect(cfg) 42 | } 43 | 44 | msnodesql.open(cfg, (err, tds) => { 45 | if (err) { 46 | err = new ConnectionError(err) 47 | return reject(err) 48 | } 49 | 50 | IDS.add(tds, 'Connection', connedtionId) 51 | tds.setUseUTC(this.config.options.useUTC) 52 | debug('connection(%d): established', IDS.get(tds)) 53 | resolve(tds) 54 | }) 55 | }) 56 | } 57 | 58 | _poolValidate (tds) { 59 | if (tds && !tds.hasError) { 60 | return !this.config.validateConnection || new shared.Promise((resolve) => { 61 | tds.query('SELECT 1;', (err) => { 62 | resolve(!err) 63 | }) 64 | }) 65 | } 66 | return false 67 | } 68 | 69 | _poolDestroy (tds) { 70 | return new shared.Promise((resolve, reject) => { 71 | if (!tds) { 72 | resolve() 73 | return 74 | } 75 | debug('connection(%d): destroying', IDS.get(tds)) 76 | tds.close(() => { 77 | debug('connection(%d): destroyed', IDS.get(tds)) 78 | resolve() 79 | }) 80 | }) 81 | } 82 | } 83 | 84 | module.exports = ConnectionPool 85 | -------------------------------------------------------------------------------- /lib/msnodesqlv8/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const base = require('../base') 4 | const ConnectionPool = require('./connection-pool') 5 | const Transaction = require('./transaction') 6 | const Request = require('./request') 7 | 8 | module.exports = Object.assign({ 9 | ConnectionPool, 10 | Transaction, 11 | Request, 12 | PreparedStatement: base.PreparedStatement 13 | }, base.exports) 14 | 15 | Object.defineProperty(module.exports, 'Promise', { 16 | enumerable: true, 17 | get: () => { 18 | return base.Promise 19 | }, 20 | set: (value) => { 21 | base.Promise = value 22 | } 23 | }) 24 | 25 | Object.defineProperty(module.exports, 'valueHandler', { 26 | enumerable: true, 27 | value: base.valueHandler, 28 | writable: false, 29 | configurable: false 30 | }) 31 | 32 | base.driver.name = 'msnodesqlv8' 33 | base.driver.ConnectionPool = ConnectionPool 34 | base.driver.Transaction = Transaction 35 | base.driver.Request = Request 36 | -------------------------------------------------------------------------------- /lib/msnodesqlv8/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const msnodesql = require('msnodesqlv8') 4 | const debug = require('debug')('mssql:msv8') 5 | const BaseRequest = require('../base/request') 6 | const RequestError = require('../error/request-error') 7 | const { IDS, objectHasProperty } = require('../utils') 8 | const { TYPES, DECLARATIONS, declare } = require('../datatypes') 9 | const { PARSERS: UDT } = require('../udt') 10 | const Table = require('../table') 11 | const { valueHandler } = require('../shared') 12 | 13 | const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B' 14 | const XML_COLUMN_ID = 'XML_F52E2B61-18A1-11d1-B105-00805F49916B' 15 | const EMPTY_BUFFER = Buffer.alloc(0) 16 | 17 | const castParameter = function (value, type) { 18 | if (value == null) { 19 | if ((type === TYPES.Binary) || (type === TYPES.VarBinary) || (type === TYPES.Image)) { 20 | // msnodesql has some problems with NULL values in those types, so we need to replace it with empty buffer 21 | return EMPTY_BUFFER 22 | } 23 | 24 | return null 25 | } 26 | 27 | switch (type) { 28 | case TYPES.VarChar: 29 | case TYPES.NVarChar: 30 | case TYPES.Char: 31 | case TYPES.NChar: 32 | case TYPES.Xml: 33 | case TYPES.Text: 34 | case TYPES.NText: 35 | if ((typeof value !== 'string') && !(value instanceof String)) { 36 | value = value.toString() 37 | } 38 | break 39 | 40 | case TYPES.Int: 41 | case TYPES.TinyInt: 42 | case TYPES.BigInt: 43 | case TYPES.SmallInt: 44 | if ((typeof value !== 'number') && !(value instanceof Number)) { 45 | value = parseInt(value) 46 | if (isNaN(value)) { value = null } 47 | } 48 | break 49 | 50 | case TYPES.Float: 51 | case TYPES.Real: 52 | case TYPES.Decimal: 53 | case TYPES.Numeric: 54 | case TYPES.SmallMoney: 55 | case TYPES.Money: 56 | if ((typeof value !== 'number') && !(value instanceof Number)) { 57 | value = parseFloat(value) 58 | if (isNaN(value)) { value = null } 59 | } 60 | break 61 | 62 | case TYPES.Bit: 63 | if ((typeof value !== 'boolean') && !(value instanceof Boolean)) { 64 | value = Boolean(value) 65 | } 66 | break 67 | 68 | case TYPES.DateTime: 69 | case TYPES.SmallDateTime: 70 | case TYPES.DateTimeOffset: 71 | case TYPES.Date: 72 | if (!(value instanceof Date)) { 73 | value = new Date(value) 74 | } 75 | break 76 | 77 | case TYPES.Binary: 78 | case TYPES.VarBinary: 79 | case TYPES.Image: 80 | if (!(value instanceof Buffer)) { 81 | value = Buffer.from(value.toString()) 82 | } 83 | break 84 | case TYPES.TVP: 85 | value = msnodesql.TvpFromTable(value) 86 | break 87 | } 88 | 89 | return value 90 | } 91 | 92 | const createColumns = function (metadata, arrayRowMode) { 93 | let out = {} 94 | if (arrayRowMode) out = [] 95 | for (let index = 0, length = metadata.length; index < length; index++) { 96 | const column = metadata[index] 97 | const colName = column.name 98 | const outColumn = { 99 | index, 100 | name: column.name, 101 | length: column.size, 102 | type: DECLARATIONS[column.sqlType], 103 | nullable: column.nullable 104 | } 105 | 106 | if (column.udtType != null) { 107 | outColumn.udt = { 108 | name: column.udtType 109 | } 110 | 111 | if (DECLARATIONS[column.udtType]) { 112 | outColumn.type = DECLARATIONS[column.udtType] 113 | } 114 | } 115 | if (arrayRowMode) { 116 | out.push(outColumn) 117 | } else { 118 | out[colName] = outColumn 119 | } 120 | } 121 | 122 | return out 123 | } 124 | 125 | const valueCorrection = function (value, metadata) { 126 | const type = metadata && objectHasProperty(metadata, 'sqlType') && objectHasProperty(DECLARATIONS, metadata.sqlType) 127 | ? DECLARATIONS[metadata.sqlType] 128 | : null 129 | if (type && valueHandler.has(type)) { 130 | return valueHandler.get(type)(value) 131 | } else if ((metadata.sqlType === 'time') && (value != null)) { 132 | value.setFullYear(1970) 133 | return value 134 | } else if ((metadata.sqlType === 'udt') && (value != null)) { 135 | if (UDT[metadata.udtType]) { 136 | return UDT[metadata.udtType](value) 137 | } else { 138 | return value 139 | } 140 | } else { 141 | return value 142 | } 143 | } 144 | 145 | class Request extends BaseRequest { 146 | _batch (batch, callback) { 147 | this._isBatch = true 148 | this._query(batch, callback) 149 | } 150 | 151 | _bulk (table, options, callback) { 152 | super._bulk(table, options, err => { 153 | if (err) return callback(err) 154 | 155 | try { 156 | table._makeBulk() 157 | } catch (e) { 158 | return callback(new RequestError(e, 'EREQUEST')) 159 | } 160 | 161 | if (!table.name) { 162 | setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME')) 163 | } 164 | 165 | if (table.name.charAt(0) === '@') { 166 | setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME')) 167 | } 168 | 169 | this.parent.acquire(this, (err, connection) => { 170 | let hasReturned = false 171 | if (!err) { 172 | debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this)) 173 | 174 | if (this.canceled) { 175 | debug('request(%d): canceled', IDS.get(this)) 176 | this.parent.release(connection) 177 | return callback(new RequestError('Canceled.', 'ECANCEL')) 178 | } 179 | 180 | const done = (err, rowCount) => { 181 | if (hasReturned) { 182 | return 183 | } 184 | 185 | hasReturned = true 186 | 187 | if (err) { 188 | if ((typeof err.sqlstate === 'string') && (err.sqlstate.toLowerCase() === '08s01')) { 189 | connection.hasError = true 190 | } 191 | 192 | err = new RequestError(err) 193 | err.code = 'EREQUEST' 194 | } 195 | 196 | this.parent.release(connection) 197 | 198 | if (err) { 199 | callback(err) 200 | } else { 201 | callback(null, table.rows.length) 202 | } 203 | } 204 | 205 | const go = () => { 206 | const tm = connection.tableMgr() 207 | return tm.bind(table.path.replace(/\[|\]/g, ''), mgr => { 208 | if (mgr.columns.length === 0) { 209 | return done(new RequestError('Table was not found on the server.', 'ENAME')) 210 | } 211 | 212 | const rows = [] 213 | for (const row of Array.from(table.rows)) { 214 | const item = {} 215 | for (let index = 0; index < table.columns.length; index++) { 216 | const col = table.columns[index] 217 | item[col.name] = row[index] 218 | } 219 | 220 | rows.push(item) 221 | } 222 | 223 | mgr.insertRows(rows, done) 224 | }) 225 | } 226 | 227 | if (table.create) { 228 | let objectid 229 | if (table.temporary) { 230 | objectid = `tempdb..[${table.name}]` 231 | } else { 232 | objectid = table.path 233 | } 234 | 235 | return connection.queryRaw(`if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, function (err) { 236 | if (err) { return done(err) } 237 | go() 238 | }) 239 | } else { 240 | go() 241 | } 242 | } 243 | }) 244 | }) 245 | } 246 | 247 | _query (command, callback) { 248 | super._query(command, err => { 249 | if (err) return callback(err) 250 | 251 | if (command.length === 0) { 252 | return callback(null, []) 253 | } 254 | 255 | const recordsets = [] 256 | const recordsetcolumns = [] 257 | const errors = [] 258 | const errorHandlers = {} 259 | const output = {} 260 | const rowsAffected = [] 261 | 262 | let hasReturned = false 263 | let row = null 264 | let columns = null 265 | let recordset = null 266 | let handleOutput = false 267 | let isChunkedRecordset = false 268 | let chunksBuffer = null 269 | 270 | const handleError = (req, connection, info, moreErrors) => { 271 | const doReturn = !moreErrors 272 | if ((typeof info.sqlstate === 'string') && (info.sqlstate.toLowerCase() === '08s01')) { 273 | connection.hasError = true 274 | } 275 | 276 | const err = new RequestError(info, 'EREQUEST') 277 | err.code = 'EREQUEST' 278 | 279 | if (this.stream) { 280 | this.emit('error', err) 281 | } else { 282 | if (doReturn && !hasReturned) { 283 | if (req) { 284 | for (const event in errorHandlers) { 285 | req.removeListener(event, errorHandlers[event]) 286 | } 287 | } 288 | if (connection) { 289 | this.parent.release(connection) 290 | delete this._cancel 291 | 292 | debug('request(%d): failed', IDS.get(this), err) 293 | } 294 | 295 | let previous 296 | if (errors.length) { 297 | previous = errors.pop() 298 | if (!err.precedingErrors) { 299 | err.precedingErrors = [] 300 | } 301 | err.precedingErrors.push(previous) 302 | } 303 | 304 | hasReturned = true 305 | callback(err) 306 | } 307 | } 308 | 309 | // we must collect errors even in stream mode 310 | errors.push(err) 311 | } 312 | 313 | // nested = function is called by this.execute 314 | 315 | if (!this._nested) { 316 | const input = [] 317 | for (const name in this.parameters) { 318 | if (!objectHasProperty(this.parameters, name)) { 319 | continue 320 | } 321 | const param = this.parameters[name] 322 | input.push(`@${param.name} ${declare(param.type, param)}`) 323 | } 324 | 325 | const sets = [] 326 | for (const name in this.parameters) { 327 | if (!objectHasProperty(this.parameters, name)) { 328 | continue 329 | } 330 | const param = this.parameters[name] 331 | if (param.io === 1) { 332 | sets.push(`set @${param.name}=?`) 333 | } 334 | } 335 | 336 | const output = [] 337 | for (const name in this.parameters) { 338 | if (!objectHasProperty(this.parameters, name)) { 339 | continue 340 | } 341 | const param = this.parameters[name] 342 | if (param.io === 2) { 343 | output.push(`@${param.name} as '${param.name}'`) 344 | } 345 | } 346 | 347 | if (input.length) command = `declare ${input.join(',')};${sets.join(';')};${command};` 348 | if (output.length) { 349 | command += `select ${output.join(',')};` 350 | handleOutput = true 351 | } 352 | } 353 | 354 | this.parent.acquire(this, (err, connection, config) => { 355 | if (err) return callback(err) 356 | 357 | debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this)) 358 | 359 | if (this.canceled) { 360 | debug('request(%d): canceled', IDS.get(this)) 361 | this.parent.release(connection) 362 | return callback(new RequestError('Canceled.', 'ECANCEL')) 363 | } 364 | 365 | const params = [] 366 | for (const name in this.parameters) { 367 | if (!objectHasProperty(this.parameters, name)) { 368 | continue 369 | } 370 | const param = this.parameters[name] 371 | if (param.io === 1 || (param.io === 2 && param.value)) { 372 | params.push(castParameter(param.value, param.type)) 373 | } 374 | } 375 | 376 | debug('request(%d): query', IDS.get(this), command) 377 | 378 | const req = connection.queryRaw({ 379 | query_str: command, 380 | query_timeout: config.requestTimeout / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported) 381 | }, params) 382 | 383 | this._setCurrentRequest(req) 384 | 385 | this._cancel = () => { 386 | debug('request(%d): cancel', IDS.get(this)) 387 | req.cancelQuery(err => { 388 | if (err) debug('request(%d): failed to cancel', IDS.get(this), err) 389 | // this fixes an issue where paused connections don't emit a done event 390 | try { 391 | if (req.isPaused()) req.emit('done') 392 | } catch (err) { 393 | // do nothing 394 | } 395 | }) 396 | } 397 | 398 | req.on('meta', metadata => { 399 | if (row) { 400 | if (isChunkedRecordset) { 401 | const concatenatedChunks = chunksBuffer.join('') 402 | if ((columns[0].name === JSON_COLUMN_ID) && (config.parseJSON === true)) { 403 | try { 404 | if (concatenatedChunks === '') { 405 | row = null 406 | } else { 407 | row = JSON.parse(concatenatedChunks) 408 | } 409 | if (!this.stream) { recordsets[recordsets.length - 1][0] = row } 410 | } catch (ex) { 411 | row = null 412 | const ex2 = new RequestError(`Failed to parse incoming JSON. ${ex.message}`, 'EJSON') 413 | 414 | if (this.stream) { 415 | this.emit('error', ex2) 416 | } else { 417 | console.error(ex2) 418 | } 419 | } 420 | } else { 421 | row[columns[0].name] = concatenatedChunks 422 | } 423 | 424 | chunksBuffer = null 425 | if (row && row.___return___ == null) { 426 | // row with ___return___ col is the last row 427 | if (this.stream && !this.paused) this.emit('row', row) 428 | } 429 | } 430 | } 431 | 432 | row = null 433 | columns = metadata 434 | recordset = [] 435 | 436 | Object.defineProperty(recordset, 'columns', { 437 | enumerable: false, 438 | configurable: true, 439 | value: createColumns(metadata, this.arrayRowMode) 440 | }) 441 | 442 | Object.defineProperty(recordset, 'toTable', { 443 | enumerable: false, 444 | configurable: true, 445 | value (name) { return Table.fromRecordset(this, name) } 446 | }) 447 | 448 | isChunkedRecordset = false 449 | if ((metadata.length === 1) && (metadata[0].name === JSON_COLUMN_ID || metadata[0].name === XML_COLUMN_ID)) { 450 | isChunkedRecordset = true 451 | chunksBuffer = [] 452 | } 453 | 454 | let hasReturnColumn = false 455 | if (recordset.columns.___return___ != null) { 456 | hasReturnColumn = true 457 | } else if (this.arrayRowMode) { 458 | for (let i = 0; i < columns.length; i++) { 459 | if (columns[i].name === '___return___') { 460 | hasReturnColumn = true 461 | break 462 | } 463 | } 464 | } 465 | if (this.stream) { 466 | if (!hasReturnColumn) { 467 | this.emit('recordset', recordset.columns) 468 | } 469 | } else { 470 | recordsets.push(recordset) 471 | } 472 | if (this.arrayRowMode) recordsetcolumns.push(recordset.columns) 473 | }) 474 | 475 | req.on('row', rownumber => { 476 | if (row && isChunkedRecordset) return 477 | 478 | if (this.arrayRowMode) { 479 | row = [] 480 | } else { 481 | row = {} 482 | } 483 | 484 | if (!this.stream) recordset.push(row) 485 | }) 486 | 487 | req.on('column', (idx, data, more) => { 488 | if (isChunkedRecordset) { 489 | chunksBuffer.push(data) 490 | } else { 491 | data = valueCorrection(data, columns[idx]) 492 | 493 | if (this.arrayRowMode) { 494 | row.push(data) 495 | } else { 496 | const exi = row[columns[idx].name] 497 | if (exi != null) { 498 | if (exi instanceof Array) { 499 | exi.push(data) 500 | } else { 501 | row[columns[idx].name] = [exi, data] 502 | } 503 | } else { 504 | row[columns[idx].name] = data 505 | } 506 | } 507 | let hasReturnColumn = false 508 | if (row && row.___return___ != null) { 509 | hasReturnColumn = true 510 | } else if (this.arrayRowMode) { 511 | for (let i = 0; i < columns.length; i++) { 512 | if (columns[i].name === '___return___') { 513 | hasReturnColumn = true 514 | break 515 | } 516 | } 517 | } 518 | if (!hasReturnColumn) { 519 | if (this.stream && !this.paused && idx === columns.length - 1) { 520 | this.emit('row', row) 521 | } 522 | } 523 | } 524 | }) 525 | 526 | req.on('rowcount', rowCount => { 527 | rowsAffected.push(rowCount) 528 | if (this.stream) { 529 | this.emit('rowsaffected', rowCount) 530 | } 531 | }) 532 | 533 | req.on('info', msg => { 534 | const parsedMessage = (/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec(msg.message) 535 | if (parsedMessage) { 536 | msg.message = parsedMessage[1] 537 | } 538 | 539 | this.emit('info', { 540 | message: msg.message, 541 | number: msg.code, 542 | state: msg.sqlstate, 543 | class: msg.class || 0, 544 | lineNumber: msg.lineNumber || 0, 545 | serverName: msg.serverName, 546 | procName: msg.procName 547 | }) 548 | 549 | // query terminated 550 | if (msg.code === 3621 && !hasReturned) { 551 | // if the query has been terminated it's probably best to throw the last meaningful error if there was one 552 | // pop it off the errors array so it doesn't get put in twice 553 | const error = errors.length > 0 ? errors.pop() : msg 554 | handleError(req, connection, error.originalError || error, false) 555 | } 556 | }) 557 | 558 | req.on('error', errorHandlers.error = handleError.bind(null, req, connection)) 559 | 560 | req.once('done', () => { 561 | if (hasReturned) { 562 | return 563 | } 564 | 565 | hasReturned = true 566 | 567 | if (!this._nested) { 568 | if (row) { 569 | if (isChunkedRecordset) { 570 | const concatenatedChunks = chunksBuffer.join('') 571 | if ((columns[0].name === JSON_COLUMN_ID) && (config.parseJSON === true)) { 572 | try { 573 | if (concatenatedChunks === '') { 574 | row = null 575 | } else { 576 | row = JSON.parse(concatenatedChunks) 577 | } 578 | if (!this.stream) { recordsets[recordsets.length - 1][0] = row } 579 | } catch (ex) { 580 | row = null 581 | const ex2 = new RequestError(`Failed to parse incoming JSON. ${ex.message}`, 'EJSON') 582 | 583 | if (this.stream) { 584 | this.emit('error', ex2) 585 | } else { 586 | console.error(ex2) 587 | } 588 | } 589 | } else { 590 | row[columns[0].name] = concatenatedChunks 591 | } 592 | 593 | chunksBuffer = null 594 | if (row && row.___return___ == null) { 595 | // row with ___return___ col is the last row 596 | if (this.stream && !this.paused) { this.emit('row', row) } 597 | } 598 | } 599 | } 600 | 601 | // do we have output parameters to handle? 602 | if (handleOutput && recordsets.length) { 603 | const last = recordsets.pop()[0] 604 | 605 | for (const name in this.parameters) { 606 | if (!objectHasProperty(this.parameters, name)) { 607 | continue 608 | } 609 | const param = this.parameters[name] 610 | if (param.io === 2) { 611 | output[param.name] = last[param.name] 612 | } 613 | } 614 | } 615 | } 616 | 617 | delete this._cancel 618 | this.parent.release(connection) 619 | 620 | debug('request(%d): completed', IDS.get(this)) 621 | 622 | if (this.stream) { 623 | callback(null, this._nested ? row : null, output, rowsAffected, recordsetcolumns) 624 | } else { 625 | callback(null, recordsets, output, rowsAffected, recordsetcolumns) 626 | } 627 | }) 628 | }) 629 | }) 630 | } 631 | 632 | _execute (procedure, callback) { 633 | super._execute(procedure, err => { 634 | if (err) return callback(err) 635 | 636 | const params = [] 637 | for (const name in this.parameters) { 638 | if (!objectHasProperty(this.parameters, name)) { 639 | continue 640 | } 641 | const param = this.parameters[name] 642 | if (param.io === 2) { 643 | params.push(`@${param.name} ${declare(param.type, param)}`) 644 | } 645 | } 646 | 647 | // set output params w/ values 648 | const sets = [] 649 | for (const name in this.parameters) { 650 | if (!objectHasProperty(this.parameters, name)) { 651 | continue 652 | } 653 | const param = this.parameters[name] 654 | if (param.io === 2 && param.value) { 655 | sets.push(`set @${param.name}=?`) 656 | } 657 | } 658 | 659 | let cmd = `declare ${['@___return___ int'].concat(params).join(', ')};${sets.join(';')};` 660 | cmd += `exec @___return___ = ${procedure} ` 661 | 662 | const spp = [] 663 | for (const name in this.parameters) { 664 | if (!objectHasProperty(this.parameters, name)) { 665 | continue 666 | } 667 | const param = this.parameters[name] 668 | 669 | if (param.io === 2) { 670 | // output parameter 671 | spp.push(`@${param.name}=@${param.name} output`) 672 | } else { 673 | // input parameter 674 | spp.push(`@${param.name}=?`) 675 | } 676 | } 677 | 678 | const params2 = [] 679 | for (const name in this.parameters) { 680 | if (!objectHasProperty(this.parameters, name)) { 681 | continue 682 | } 683 | const param = this.parameters[name] 684 | if (param.io === 2) { 685 | params2.push(`@${param.name} as '${param.name}'`) 686 | } 687 | } 688 | 689 | cmd += `${spp.join(', ')};` 690 | cmd += `select ${['@___return___ as \'___return___\''].concat(params2).join(', ')};` 691 | 692 | this._nested = true 693 | 694 | this._query(cmd, (err, recordsets, output, rowsAffected, recordsetcolumns) => { 695 | this._nested = false 696 | 697 | if (err) return callback(err) 698 | 699 | let last, returnValue 700 | if (this.stream) { 701 | last = recordsets 702 | } else { 703 | last = recordsets.pop() 704 | if (last) last = last[0] 705 | } 706 | const lastColumns = recordsetcolumns.pop() 707 | 708 | if (last && this.arrayRowMode && lastColumns) { 709 | let returnColumnIdx = null 710 | const parametersNameToLastIdxDict = {} 711 | for (let i = 0; i < lastColumns.length; i++) { 712 | if (lastColumns[i].name === '___return___') { 713 | returnColumnIdx = i 714 | } else if (objectHasProperty(this.parameters, lastColumns[i].name)) { 715 | parametersNameToLastIdxDict[lastColumns[i].name] = i 716 | } 717 | } 718 | if (returnColumnIdx != null) { 719 | returnValue = last[returnColumnIdx] 720 | } 721 | for (const name in parametersNameToLastIdxDict) { 722 | if (!objectHasProperty(parametersNameToLastIdxDict, name)) { 723 | continue 724 | } 725 | const param = this.parameters[name] 726 | if (param.io === 2) { 727 | output[param.name] = last[parametersNameToLastIdxDict[name]] 728 | } 729 | } 730 | } else { 731 | if (last && (last.___return___ != null)) { 732 | returnValue = last.___return___ 733 | 734 | for (const name in this.parameters) { 735 | if (!objectHasProperty(this.parameters, name)) { 736 | continue 737 | } 738 | const param = this.parameters[name] 739 | if (param.io === 2) { 740 | output[param.name] = last[param.name] 741 | } 742 | } 743 | } 744 | } 745 | if (this.stream) { 746 | callback(null, null, output, returnValue, rowsAffected, recordsetcolumns) 747 | } else { 748 | callback(null, recordsets, output, returnValue, rowsAffected, recordsetcolumns) 749 | } 750 | }) 751 | }) 752 | } 753 | 754 | _pause () { 755 | super._pause() 756 | if (this._currentRequest) { 757 | this._currentRequest.pauseQuery() 758 | } 759 | } 760 | 761 | _resume () { 762 | super._resume() 763 | if (this._currentRequest) { 764 | this._currentRequest.resumeQuery() 765 | } 766 | } 767 | } 768 | 769 | module.exports = Request 770 | -------------------------------------------------------------------------------- /lib/msnodesqlv8/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('mssql:msv8') 4 | const BaseTransaction = require('../base/transaction') 5 | const { IDS } = require('../utils') 6 | const Request = require('./request') 7 | const ISOLATION_LEVEL = require('../isolationlevel') 8 | const TransactionError = require('../error/transaction-error') 9 | 10 | const isolationLevelDeclaration = function (type) { 11 | switch (type) { 12 | case ISOLATION_LEVEL.READ_UNCOMMITTED: return 'READ UNCOMMITTED' 13 | case ISOLATION_LEVEL.READ_COMMITTED: return 'READ COMMITTED' 14 | case ISOLATION_LEVEL.REPEATABLE_READ: return 'REPEATABLE READ' 15 | case ISOLATION_LEVEL.SERIALIZABLE: return 'SERIALIZABLE' 16 | case ISOLATION_LEVEL.SNAPSHOT: return 'SNAPSHOT' 17 | default: throw new TransactionError('Invalid isolation level.') 18 | } 19 | } 20 | 21 | class Transaction extends BaseTransaction { 22 | _begin (isolationLevel, callback) { 23 | super._begin(isolationLevel, err => { 24 | if (err) return callback(err) 25 | 26 | debug('transaction(%d): begin', IDS.get(this)) 27 | 28 | this.parent.acquire(this, (err, connection, config) => { 29 | if (err) return callback(err) 30 | 31 | this._acquiredConnection = connection 32 | this._acquiredConfig = config 33 | 34 | const req = new Request(this) 35 | req.stream = false 36 | req.query(`set transaction isolation level ${isolationLevelDeclaration(this.isolationLevel)};begin tran;`, err => { 37 | if (err) { 38 | this.parent.release(this._acquiredConnection) 39 | this._acquiredConnection = null 40 | this._acquiredConfig = null 41 | 42 | return callback(err) 43 | } 44 | 45 | debug('transaction(%d): begun', IDS.get(this)) 46 | 47 | callback(null) 48 | }) 49 | }) 50 | }) 51 | } 52 | 53 | _commit (callback) { 54 | super._commit(err => { 55 | if (err) return callback(err) 56 | 57 | debug('transaction(%d): commit', IDS.get(this)) 58 | 59 | const req = new Request(this) 60 | req.stream = false 61 | req.query('commit tran', err => { 62 | if (err) err = new TransactionError(err) 63 | 64 | this.parent.release(this._acquiredConnection) 65 | this._acquiredConnection = null 66 | this._acquiredConfig = null 67 | 68 | if (!err) debug('transaction(%d): commited', IDS.get(this)) 69 | 70 | callback(null) 71 | }) 72 | }) 73 | } 74 | 75 | _rollback (callback) { 76 | super._commit(err => { 77 | if (err) return callback(err) 78 | 79 | debug('transaction(%d): rollback', IDS.get(this)) 80 | 81 | const req = new Request(this) 82 | req.stream = false 83 | req.query('rollback tran', err => { 84 | if (err) err = new TransactionError(err) 85 | 86 | this.parent.release(this._acquiredConnection) 87 | this._acquiredConnection = null 88 | this._acquiredConfig = null 89 | 90 | if (!err) debug('transaction(%d): rolled back', IDS.get(this)) 91 | 92 | callback(null) 93 | }) 94 | }) 95 | } 96 | } 97 | 98 | module.exports = Transaction 99 | -------------------------------------------------------------------------------- /lib/shared.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { TYPES } = require('./datatypes') 4 | const Table = require('./table') 5 | 6 | let PromiseLibrary = Promise 7 | const driver = {} 8 | const map = [] 9 | 10 | /** 11 | * Register you own type map. 12 | * 13 | * @path module.exports.map 14 | * @param {*} jstype JS data type. 15 | * @param {*} sqltype SQL data type. 16 | */ 17 | 18 | map.register = function (jstype, sqltype) { 19 | for (let index = 0; index < this.length; index++) { 20 | const item = this[index] 21 | if (item.js === jstype) { 22 | this.splice(index, 1) 23 | break 24 | } 25 | } 26 | 27 | this.push({ 28 | js: jstype, 29 | sql: sqltype 30 | }) 31 | 32 | return null 33 | } 34 | 35 | map.register(String, TYPES.NVarChar) 36 | map.register(Number, TYPES.Int) 37 | map.register(Boolean, TYPES.Bit) 38 | map.register(Date, TYPES.DateTime) 39 | map.register(Buffer, TYPES.VarBinary) 40 | map.register(Table, TYPES.TVP) 41 | 42 | /** 43 | * @ignore 44 | */ 45 | 46 | const getTypeByValue = function (value) { 47 | if ((value === null) || (value === undefined)) { return TYPES.NVarChar } 48 | 49 | switch (typeof value) { 50 | case 'string': 51 | for (const item of Array.from(map)) { 52 | if (item.js === String) { 53 | return item.sql 54 | } 55 | } 56 | 57 | return TYPES.NVarChar 58 | 59 | case 'number': 60 | if (value % 1 === 0) { 61 | if (value < -2147483648 || value > 2147483647) { 62 | return TYPES.BigInt 63 | } else { 64 | return TYPES.Int 65 | } 66 | } else { 67 | return TYPES.Float 68 | } 69 | 70 | case 'bigint': 71 | if (value < -2147483648n || value > 2147483647n) { 72 | return TYPES.BigInt 73 | } else { 74 | return TYPES.Int 75 | } 76 | 77 | case 'boolean': 78 | for (const item of Array.from(map)) { 79 | if (item.js === Boolean) { 80 | return item.sql 81 | } 82 | } 83 | 84 | return TYPES.Bit 85 | 86 | case 'object': 87 | for (const item of Array.from(map)) { 88 | if (value instanceof item.js) { 89 | return item.sql 90 | } 91 | } 92 | 93 | return TYPES.NVarChar 94 | 95 | default: 96 | return TYPES.NVarChar 97 | } 98 | } 99 | 100 | module.exports = { 101 | driver, 102 | getTypeByValue, 103 | map 104 | } 105 | 106 | Object.defineProperty(module.exports, 'Promise', { 107 | get: () => { 108 | return PromiseLibrary 109 | }, 110 | set: (value) => { 111 | PromiseLibrary = value 112 | } 113 | }) 114 | 115 | Object.defineProperty(module.exports, 'valueHandler', { 116 | enumerable: true, 117 | value: new Map(), 118 | writable: false, 119 | configurable: false 120 | }) 121 | -------------------------------------------------------------------------------- /lib/table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TYPES = require('./datatypes').TYPES 4 | const declareType = require('./datatypes').declare 5 | const objectHasProperty = require('./utils').objectHasProperty 6 | 7 | const MAX = 65535 // (1 << 16) - 1 8 | const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B' 9 | 10 | function Table (name) { 11 | if (name) { 12 | const parsed = Table.parseName(name) 13 | this.name = parsed.name 14 | this.schema = parsed.schema 15 | this.database = parsed.database 16 | this.path = (this.database ? `[${this.database}].` : '') + (this.schema ? `[${this.schema}].` : '') + `[${this.name}]` 17 | this.temporary = this.name.charAt(0) === '#' 18 | } 19 | 20 | this.columns = [] 21 | this.rows = [] 22 | 23 | Object.defineProperty(this.columns, 'add', { 24 | value (name, column, options) { 25 | if (column == null) { 26 | throw new Error('Column data type is not defined.') 27 | } 28 | if (column instanceof Function) { 29 | column = column() 30 | } 31 | 32 | options = options || {} 33 | column.name = name; 34 | 35 | ['nullable', 'primary', 'identity', 'readOnly', 'length'].forEach(prop => { 36 | if (objectHasProperty(options, prop)) { 37 | column[prop] = options[prop] 38 | } 39 | }) 40 | 41 | return this.push(column) 42 | } 43 | }) 44 | 45 | Object.defineProperty(this.rows, 'add', { 46 | value () { 47 | return this.push(Array.prototype.slice.call(arguments)) 48 | } 49 | } 50 | ) 51 | 52 | Object.defineProperty(this.rows, 'clear', { 53 | value () { 54 | return this.splice(0, this.length) 55 | } 56 | } 57 | ) 58 | } 59 | 60 | /* 61 | @private 62 | */ 63 | 64 | Table.prototype._makeBulk = function _makeBulk () { 65 | for (let i = 0; i < this.columns.length; i++) { 66 | const col = this.columns[i] 67 | switch (col.type) { 68 | case TYPES.Date: 69 | case TYPES.DateTime: 70 | case TYPES.DateTime2: 71 | for (let j = 0; j < this.rows.length; j++) { 72 | const dateValue = this.rows[j][i] 73 | if (typeof dateValue === 'string' || typeof dateValue === 'number') { 74 | const date = new Date(dateValue) 75 | if (isNaN(date.getDate())) { 76 | throw new TypeError('Invalid date value passed to bulk rows') 77 | } 78 | this.rows[j][i] = date 79 | } 80 | } 81 | break 82 | 83 | case TYPES.Xml: 84 | col.type = TYPES.NVarChar(MAX).type 85 | break 86 | 87 | case TYPES.UDT: 88 | case TYPES.Geography: 89 | case TYPES.Geometry: 90 | col.type = TYPES.VarBinary(MAX).type 91 | break 92 | 93 | default: 94 | break 95 | } 96 | } 97 | 98 | return this 99 | } 100 | 101 | Table.prototype.declare = function declare () { 102 | const pkey = this.columns.filter(col => col.primary === true).map(col => `[${col.name}]`) 103 | const cols = this.columns.map(col => { 104 | const def = [`[${col.name}] ${declareType(col.type, col)}`] 105 | 106 | if (col.nullable === true) { 107 | def.push('null') 108 | } else if (col.nullable === false) { 109 | def.push('not null') 110 | } 111 | 112 | if (col.primary === true && pkey.length === 1) { 113 | def.push('primary key') 114 | } 115 | 116 | return def.join(' ') 117 | }) 118 | 119 | const constraint = pkey.length > 1 ? `, constraint [PK_${this.temporary ? this.name.substr(1) : this.name}] primary key (${pkey.join(', ')})` : '' 120 | return `create table ${this.path} (${cols.join(', ')}${constraint})` 121 | } 122 | 123 | Table.fromRecordset = function fromRecordset (recordset, name) { 124 | const t = new this(name) 125 | 126 | for (const colName in recordset.columns) { 127 | if (objectHasProperty(recordset.columns, colName)) { 128 | const col = recordset.columns[colName] 129 | 130 | t.columns.add(colName, { 131 | type: col.type, 132 | length: col.length, 133 | scale: col.scale, 134 | precision: col.precision 135 | }, { 136 | nullable: col.nullable, 137 | identity: col.identity, 138 | readOnly: col.readOnly 139 | }) 140 | } 141 | } 142 | 143 | if (t.columns.length === 1 && t.columns[0].name === JSON_COLUMN_ID) { 144 | for (let i = 0; i < recordset.length; i++) { 145 | t.rows.add(JSON.stringify(recordset[i])) 146 | } 147 | } else { 148 | for (let i = 0; i < recordset.length; i++) { 149 | t.rows.add.apply(t.rows, t.columns.map(col => recordset[i][col.name])) 150 | } 151 | } 152 | 153 | return t 154 | } 155 | 156 | Table.parseName = function parseName (name) { 157 | const length = name.length 158 | let cursor = -1 159 | let buffer = '' 160 | let escaped = false 161 | const path = [] 162 | 163 | while (++cursor < length) { 164 | const char = name.charAt(cursor) 165 | if (char === '[') { 166 | if (escaped) { 167 | buffer += char 168 | } else { 169 | escaped = true 170 | } 171 | } else if (char === ']') { 172 | if (escaped) { 173 | escaped = false 174 | } else { 175 | throw new Error('Invalid table name.') 176 | } 177 | } else if (char === '.') { 178 | if (escaped) { 179 | buffer += char 180 | } else { 181 | path.push(buffer) 182 | buffer = '' 183 | } 184 | } else { 185 | buffer += char 186 | } 187 | } 188 | 189 | if (buffer) { 190 | path.push(buffer) 191 | } 192 | 193 | switch (path.length) { 194 | case 1: 195 | return { 196 | name: path[0], 197 | schema: null, 198 | database: null 199 | } 200 | 201 | case 2: 202 | return { 203 | name: path[1], 204 | schema: path[0], 205 | database: null 206 | } 207 | 208 | case 3: 209 | return { 210 | name: path[2], 211 | schema: path[1], 212 | database: path[0] 213 | } 214 | 215 | default: 216 | throw new Error('Invalid table name.') 217 | } 218 | } 219 | 220 | module.exports = Table 221 | -------------------------------------------------------------------------------- /lib/tedious/connection-pool.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tds = require('tedious') 4 | const debug = require('debug')('mssql:tedi') 5 | const BaseConnectionPool = require('../base/connection-pool') 6 | const { IDS } = require('../utils') 7 | const shared = require('../shared') 8 | const ConnectionError = require('../error/connection-error') 9 | 10 | class ConnectionPool extends BaseConnectionPool { 11 | _config () { 12 | const cfg = { 13 | server: this.config.server, 14 | options: Object.assign({ 15 | encrypt: typeof this.config.encrypt === 'boolean' ? this.config.encrypt : true, 16 | trustServerCertificate: typeof this.config.trustServerCertificate === 'boolean' ? this.config.trustServerCertificate : false 17 | }, this.config.options), 18 | authentication: Object.assign({ 19 | type: this.config.domain !== undefined ? 'ntlm' : this.config.authentication_type !== undefined ? this.config.authentication_type : 'default', 20 | options: Object.entries({ 21 | userName: this.config.user, 22 | password: this.config.password, 23 | domain: this.config.domain, 24 | clientId: this.config.clientId, 25 | clientSecret: this.config.clientSecret, 26 | tenantId: this.config.tenantId, 27 | token: this.config.token, 28 | msiEndpoint: this.config.msiEndpoint, 29 | msiSecret: this.config.msiSecret 30 | }).reduce((acc, [key, val]) => { 31 | if (typeof val !== 'undefined') { 32 | return { ...acc, [key]: val } 33 | } 34 | return acc 35 | }, {}) 36 | }, this.config.authentication) 37 | } 38 | 39 | cfg.options.database = cfg.options.database || this.config.database 40 | cfg.options.port = cfg.options.port || this.config.port 41 | cfg.options.connectTimeout = cfg.options.connectTimeout ?? this.config.connectionTimeout ?? this.config.timeout ?? 15000 42 | cfg.options.requestTimeout = cfg.options.requestTimeout ?? this.config.requestTimeout ?? this.config.timeout ?? 15000 43 | cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4' 44 | cfg.options.rowCollectionOnDone = cfg.options.rowCollectionOnDone || false 45 | cfg.options.rowCollectionOnRequestCompletion = cfg.options.rowCollectionOnRequestCompletion || false 46 | cfg.options.useColumnNames = cfg.options.useColumnNames || false 47 | cfg.options.appName = cfg.options.appName || 'node-mssql' 48 | 49 | // tedious always connect via tcp when port is specified 50 | if (cfg.options.instanceName) delete cfg.options.port 51 | 52 | if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000 53 | if (cfg.options.requestTimeout === Infinity || cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0 54 | 55 | if (!cfg.options.debug && this.config.debug) { 56 | cfg.options.debug = { 57 | packet: true, 58 | token: true, 59 | data: true, 60 | payload: true 61 | } 62 | } 63 | return cfg 64 | } 65 | 66 | _poolCreate () { 67 | return new shared.Promise((resolve, reject) => { 68 | const resolveOnce = (v) => { 69 | resolve(v) 70 | resolve = reject = () => {} 71 | } 72 | const rejectOnce = (e) => { 73 | reject(e) 74 | resolve = reject = () => {} 75 | } 76 | let tedious 77 | try { 78 | tedious = new tds.Connection(this._config()) 79 | } catch (err) { 80 | rejectOnce(err) 81 | return 82 | } 83 | tedious.connect(err => { 84 | if (err) { 85 | err = new ConnectionError(err) 86 | return rejectOnce(err) 87 | } 88 | 89 | debug('connection(%d): established', IDS.get(tedious)) 90 | this.collation = tedious.databaseCollation 91 | resolveOnce(tedious) 92 | }) 93 | IDS.add(tedious, 'Connection') 94 | debug('pool(%d): connection #%d created', IDS.get(this), IDS.get(tedious)) 95 | debug('connection(%d): establishing', IDS.get(tedious)) 96 | 97 | tedious.on('end', () => { 98 | const err = new ConnectionError('The connection ended without ever completing the connection') 99 | rejectOnce(err) 100 | }) 101 | tedious.on('error', err => { 102 | if (err.code === 'ESOCKET') { 103 | tedious.hasError = true 104 | } else { 105 | this.emit('error', err) 106 | } 107 | rejectOnce(err) 108 | }) 109 | 110 | if (this.config.debug) { 111 | tedious.on('debug', this.emit.bind(this, 'debug', tedious)) 112 | } 113 | if (typeof this.config.beforeConnect === 'function') { 114 | this.config.beforeConnect(tedious) 115 | } 116 | }) 117 | } 118 | 119 | _poolValidate (tedious) { 120 | if (tedious && !tedious.closed && !tedious.hasError) { 121 | return !this.config.validateConnection || new shared.Promise((resolve) => { 122 | const req = new tds.Request('SELECT 1;', (err) => { 123 | resolve(!err) 124 | }) 125 | tedious.execSql(req) 126 | }) 127 | } 128 | return false 129 | } 130 | 131 | _poolDestroy (tedious) { 132 | return new shared.Promise((resolve, reject) => { 133 | if (!tedious) { 134 | resolve() 135 | return 136 | } 137 | debug('connection(%d): destroying', IDS.get(tedious)) 138 | 139 | if (tedious.closed) { 140 | debug('connection(%d): already closed', IDS.get(tedious)) 141 | resolve() 142 | } else { 143 | tedious.once('end', () => { 144 | debug('connection(%d): destroyed', IDS.get(tedious)) 145 | resolve() 146 | }) 147 | 148 | tedious.close() 149 | } 150 | }) 151 | } 152 | } 153 | 154 | module.exports = ConnectionPool 155 | -------------------------------------------------------------------------------- /lib/tedious/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const base = require('../base') 4 | const ConnectionPool = require('./connection-pool') 5 | const Transaction = require('./transaction') 6 | const Request = require('./request') 7 | 8 | module.exports = Object.assign({ 9 | ConnectionPool, 10 | Transaction, 11 | Request, 12 | PreparedStatement: base.PreparedStatement 13 | }, base.exports) 14 | 15 | Object.defineProperty(module.exports, 'Promise', { 16 | enumerable: true, 17 | get: () => { 18 | return base.Promise 19 | }, 20 | set: (value) => { 21 | base.Promise = value 22 | } 23 | }) 24 | 25 | Object.defineProperty(module.exports, 'valueHandler', { 26 | enumerable: true, 27 | value: base.valueHandler, 28 | writable: false, 29 | configurable: false 30 | }) 31 | 32 | base.driver.name = 'tedious' 33 | base.driver.ConnectionPool = ConnectionPool 34 | base.driver.Transaction = Transaction 35 | base.driver.Request = Request 36 | -------------------------------------------------------------------------------- /lib/tedious/transaction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('mssql:tedi') 4 | const BaseTransaction = require('../base/transaction') 5 | const { IDS } = require('../utils') 6 | const TransactionError = require('../error/transaction-error') 7 | 8 | class Transaction extends BaseTransaction { 9 | constructor (parent) { 10 | super(parent) 11 | 12 | this._abort = () => { 13 | if (!this._rollbackRequested) { 14 | // transaction interrupted because of XACT_ABORT 15 | 16 | const pc = this._acquiredConnection 17 | 18 | // defer releasing so connection can switch from SentClientRequest to LoggedIn state 19 | setImmediate(this.parent.release.bind(this.parent), pc) 20 | 21 | this._acquiredConnection.removeListener('rollbackTransaction', this._abort) 22 | this._acquiredConnection = null 23 | this._acquiredConfig = null 24 | this._aborted = true 25 | 26 | this.emit('rollback', true) 27 | } 28 | } 29 | } 30 | 31 | _begin (isolationLevel, callback) { 32 | super._begin(isolationLevel, err => { 33 | if (err) return callback(err) 34 | 35 | debug('transaction(%d): begin', IDS.get(this)) 36 | 37 | this.parent.acquire(this, (err, connection, config) => { 38 | if (err) return callback(err) 39 | 40 | this._acquiredConnection = connection 41 | this._acquiredConnection.on('rollbackTransaction', this._abort) 42 | this._acquiredConfig = config 43 | 44 | connection.beginTransaction(err => { 45 | if (err) err = new TransactionError(err) 46 | 47 | debug('transaction(%d): begun', IDS.get(this)) 48 | 49 | callback(err) 50 | }, this.name, this.isolationLevel) 51 | }) 52 | }) 53 | } 54 | 55 | _commit (callback) { 56 | super._commit(err => { 57 | if (err) return callback(err) 58 | 59 | debug('transaction(%d): commit', IDS.get(this)) 60 | 61 | this._acquiredConnection.commitTransaction(err => { 62 | if (err) err = new TransactionError(err) 63 | 64 | this._acquiredConnection.removeListener('rollbackTransaction', this._abort) 65 | this.parent.release(this._acquiredConnection) 66 | this._acquiredConnection = null 67 | this._acquiredConfig = null 68 | 69 | if (!err) debug('transaction(%d): commited', IDS.get(this)) 70 | 71 | callback(err) 72 | }) 73 | }) 74 | } 75 | 76 | _rollback (callback) { 77 | super._rollback(err => { 78 | if (err) return callback(err) 79 | 80 | debug('transaction(%d): rollback', IDS.get(this)) 81 | 82 | this._acquiredConnection.rollbackTransaction(err => { 83 | if (err) err = new TransactionError(err) 84 | 85 | this._acquiredConnection.removeListener('rollbackTransaction', this._abort) 86 | this.parent.release(this._acquiredConnection) 87 | this._acquiredConnection = null 88 | this._acquiredConfig = null 89 | 90 | if (!err) debug('transaction(%d): rolled back', IDS.get(this)) 91 | 92 | callback(err) 93 | }) 94 | }) 95 | } 96 | } 97 | 98 | module.exports = Transaction 99 | -------------------------------------------------------------------------------- /lib/udt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* const FIGURE = { 4 | INTERIOR_RING: 0x00, 5 | STROKE: 0x01, 6 | EXTERIOR_RING: 0x02 7 | }; 8 | 9 | const FIGURE_V2 = { 10 | POINT: 0x00, 11 | LINE: 0x01, 12 | ARC: 0x02, 13 | COMPOSITE_CURVE: 0x03 14 | }; 15 | 16 | const SHAPE = { 17 | POINT: 0x01, 18 | LINESTRING: 0x02, 19 | POLYGON: 0x03, 20 | MULTIPOINT: 0x04, 21 | MULTILINESTRING: 0x05, 22 | MULTIPOLYGON: 0x06, 23 | GEOMETRY_COLLECTION: 0x07 24 | }; 25 | 26 | const SHAPE_V2 = { 27 | POINT: 0x01, 28 | LINESTRING: 0x02, 29 | POLYGON: 0x03, 30 | MULTIPOINT: 0x04, 31 | MULTILINESTRING: 0x05, 32 | MULTIPOLYGON: 0x06, 33 | GEOMETRY_COLLECTION: 0x07, 34 | CIRCULAR_STRING: 0x08, 35 | COMPOUND_CURVE: 0x09, 36 | CURVE_POLYGON: 0x0A, 37 | FULL_GLOBE: 0x0B 38 | }; 39 | 40 | const SEGMENT = { 41 | LINE: 0x00, 42 | ARC: 0x01, 43 | FIRST_LINE: 0x02, 44 | FIRST_ARC: 0x03 45 | }; */ 46 | 47 | class Point { 48 | constructor () { 49 | this.x = 0 50 | this.y = 0 51 | this.z = null 52 | this.m = null 53 | } 54 | } 55 | 56 | const parsePoints = (buffer, count, isGeometryPoint) => { 57 | // s2.1.5 + s2.1.6 58 | // The key distinction for parsing is that a GEOGRAPHY POINT is ordered Lat (y) then Long (x), 59 | // while a GEOMETRY POINT is ordered x then y. 60 | // Further, there are additional range constraints on GEOGRAPHY POINT that are useful for testing that the coordinate order has not been flipped, such as that Lat must be in the range [-90, +90]. 61 | 62 | const points = [] 63 | if (count < 1) { 64 | return points 65 | } 66 | 67 | if (isGeometryPoint) { 68 | // GEOMETRY POINT (s2.1.6): x then y. 69 | for (let i = 1; i <= count; i++) { 70 | const point = new Point() 71 | points.push(point) 72 | point.x = buffer.readDoubleLE(buffer.position) 73 | point.y = buffer.readDoubleLE(buffer.position + 8) 74 | buffer.position += 16 75 | } 76 | } else { 77 | // GEOGRAPHY POINT (s2.1.5): Lat (y) then Long (x). 78 | for (let i = 1; i <= count; i++) { 79 | const point = new Point() 80 | points.push(point) 81 | point.lat = buffer.readDoubleLE(buffer.position) 82 | point.lng = buffer.readDoubleLE(buffer.position + 8) 83 | 84 | // For backwards compatibility, preserve the coordinate inversion in x and y. 85 | // A future breaking change likely eliminate x and y for geography points in favor of just the lat and lng fields, as they've proven marvelously confusing. 86 | // See discussion at: https://github.com/tediousjs/node-mssql/pull/1282#discussion_r677769531 87 | point.x = point.lat 88 | point.y = point.lng 89 | 90 | buffer.position += 16 91 | } 92 | } 93 | 94 | return points 95 | } 96 | 97 | const parseZ = (buffer, points) => { 98 | // s2.1.1 + s.2.1.2 99 | 100 | if (points < 1) { 101 | return 102 | } 103 | 104 | points.forEach(point => { 105 | point.z = buffer.readDoubleLE(buffer.position) 106 | buffer.position += 8 107 | }) 108 | } 109 | 110 | const parseM = (buffer, points) => { 111 | // s2.1.1 + s.2.1.2 112 | 113 | if (points < 1) { 114 | return 115 | } 116 | 117 | points.forEach(point => { 118 | point.m = buffer.readDoubleLE(buffer.position) 119 | buffer.position += 8 120 | }) 121 | } 122 | 123 | const parseFigures = (buffer, count, properties) => { 124 | // s2.1.3 125 | 126 | const figures = [] 127 | if (count < 1) { 128 | return figures 129 | } 130 | 131 | if (properties.P) { 132 | figures.push({ 133 | attribute: 0x01, 134 | pointOffset: 0 135 | }) 136 | } else if (properties.L) { 137 | figures.push({ 138 | attribute: 0x01, 139 | pointOffset: 0 140 | }) 141 | } else { 142 | for (let i = 1; i <= count; i++) { 143 | figures.push({ 144 | attribute: buffer.readUInt8(buffer.position), 145 | pointOffset: buffer.readInt32LE(buffer.position + 1) 146 | }) 147 | 148 | buffer.position += 5 149 | } 150 | } 151 | 152 | return figures 153 | } 154 | 155 | const parseShapes = (buffer, count, properties) => { 156 | // s2.1.4 157 | 158 | const shapes = [] 159 | if (count < 1) { 160 | return shapes 161 | } 162 | 163 | if (properties.P) { 164 | shapes.push({ 165 | parentOffset: -1, 166 | figureOffset: 0, 167 | type: 0x01 168 | }) 169 | } else if (properties.L) { 170 | shapes.push({ 171 | parentOffset: -1, 172 | figureOffset: 0, 173 | type: 0x02 174 | }) 175 | } else { 176 | for (let i = 1; i <= count; i++) { 177 | shapes.push({ 178 | parentOffset: buffer.readInt32LE(buffer.position), 179 | figureOffset: buffer.readInt32LE(buffer.position + 4), 180 | type: buffer.readUInt8(buffer.position + 8) 181 | }) 182 | 183 | buffer.position += 9 184 | } 185 | } 186 | 187 | return shapes 188 | } 189 | 190 | const parseSegments = (buffer, count) => { 191 | // s2.1.7 192 | 193 | const segments = [] 194 | if (count < 1) { 195 | return segments 196 | } 197 | 198 | for (let i = 1; i <= count; i++) { 199 | segments.push({ type: buffer.readUInt8(buffer.position) }) 200 | 201 | buffer.position++ 202 | } 203 | 204 | return segments 205 | } 206 | 207 | const parseGeography = (buffer, isUsingGeometryPoints) => { 208 | // s2.1.1 + s.2.1.2 209 | 210 | const srid = buffer.readInt32LE(0) 211 | if (srid === -1) { 212 | return null 213 | } 214 | 215 | const value = { 216 | srid, 217 | version: buffer.readUInt8(4) 218 | } 219 | 220 | const flags = buffer.readUInt8(5) 221 | buffer.position = 6 222 | 223 | // console.log("srid", srid) 224 | // console.log("version", version) 225 | 226 | const properties = { 227 | Z: (flags & (1 << 0)) > 0, 228 | M: (flags & (1 << 1)) > 0, 229 | V: (flags & (1 << 2)) > 0, 230 | P: (flags & (1 << 3)) > 0, 231 | L: (flags & (1 << 4)) > 0 232 | } 233 | 234 | if (value.version === 2) { 235 | properties.H = (flags & (1 << 3)) > 0 236 | } 237 | 238 | // console.log("properties", properties); 239 | 240 | let numberOfPoints 241 | if (properties.P) { 242 | numberOfPoints = 1 243 | } else if (properties.L) { 244 | numberOfPoints = 2 245 | } else { 246 | numberOfPoints = buffer.readUInt32LE(buffer.position) 247 | buffer.position += 4 248 | } 249 | 250 | // console.log("numberOfPoints", numberOfPoints) 251 | 252 | value.points = parsePoints(buffer, numberOfPoints, isUsingGeometryPoints) 253 | 254 | if (properties.Z) { 255 | parseZ(buffer, value.points) 256 | } 257 | 258 | if (properties.M) { 259 | parseM(buffer, value.points) 260 | } 261 | 262 | // console.log("points", points) 263 | 264 | let numberOfFigures 265 | if (properties.P) { 266 | numberOfFigures = 1 267 | } else if (properties.L) { 268 | numberOfFigures = 1 269 | } else { 270 | numberOfFigures = buffer.readUInt32LE(buffer.position) 271 | buffer.position += 4 272 | } 273 | 274 | // console.log("numberOfFigures", numberOfFigures) 275 | 276 | value.figures = parseFigures(buffer, numberOfFigures, properties) 277 | 278 | // console.log("figures", figures) 279 | 280 | let numberOfShapes 281 | if (properties.P) { 282 | numberOfShapes = 1 283 | } else if (properties.L) { 284 | numberOfShapes = 1 285 | } else { 286 | numberOfShapes = buffer.readUInt32LE(buffer.position) 287 | buffer.position += 4 288 | } 289 | 290 | // console.log("numberOfShapes", numberOfShapes) 291 | 292 | value.shapes = parseShapes(buffer, numberOfShapes, properties) 293 | 294 | // console.log( "shapes", shapes) 295 | 296 | if (value.version === 2 && buffer.position < buffer.length) { 297 | const numberOfSegments = buffer.readUInt32LE(buffer.position) 298 | buffer.position += 4 299 | 300 | // console.log("numberOfSegments", numberOfSegments) 301 | 302 | value.segments = parseSegments(buffer, numberOfSegments) 303 | 304 | // console.log("segments", segments) 305 | } else { 306 | value.segments = [] 307 | } 308 | 309 | return value 310 | } 311 | 312 | module.exports.PARSERS = { 313 | geography (buffer) { 314 | return parseGeography(buffer, /* isUsingGeometryPoints: */false) 315 | }, 316 | 317 | geometry (buffer) { 318 | return parseGeography(buffer, /* isUsingGeometryPoints: */true) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const IDS = new WeakMap() 4 | const INCREMENT = { 5 | Connection: 1, 6 | ConnectionPool: 1, 7 | Request: 1, 8 | Transaction: 1, 9 | PreparedStatement: 1 10 | } 11 | 12 | module.exports = { 13 | objectHasProperty: (object, property) => Object.prototype.hasOwnProperty.call(object, property), 14 | INCREMENT, 15 | IDS: { 16 | get: IDS.get.bind(IDS), 17 | add: (object, type, id) => { 18 | if (id) return IDS.set(object, id) 19 | IDS.set(object, INCREMENT[type]++) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /msnodesqlv8.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/msnodesqlv8') 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Patrik Simek", 4 | "url": "https://patriksimek.cz" 5 | }, 6 | "name": "mssql", 7 | "description": "Microsoft SQL Server client for Node.js.", 8 | "keywords": [ 9 | "database", 10 | "mssql", 11 | "sql", 12 | "server", 13 | "msnodesql", 14 | "sqlserver", 15 | "tds", 16 | "node-tds", 17 | "tedious", 18 | "node-sqlserver", 19 | "sqlserver", 20 | "msnodesqlv8", 21 | "azure", 22 | "node-mssql" 23 | ], 24 | "version": "9.1.1", 25 | "main": "index.js", 26 | "type": "commonjs", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/tediousjs/node-mssql.git" 30 | }, 31 | "homepage": "https://github.com/tediousjs/node-mssql#readme", 32 | "bugs": { 33 | "url": "https://github.com/tediousjs/node-mssql/issues" 34 | }, 35 | "license": "MIT", 36 | "dependencies": { 37 | "@tediousjs/connection-string": "^0.6.0", 38 | "commander": "^11.0.0", 39 | "debug": "^4.3.3", 40 | "rfdc": "^1.3.0", 41 | "tarn": "^3.0.2", 42 | "tedious": "^19.0.0" 43 | }, 44 | "devDependencies": { 45 | "@commitlint/cli": "^19.3.0", 46 | "@commitlint/config-conventional": "^19.2.2", 47 | "@semantic-release/commit-analyzer": "^11.1.0", 48 | "@semantic-release/github": "^9.2.6", 49 | "@semantic-release/npm": "^11.0.3", 50 | "@semantic-release/release-notes-generator": "^12.1.0", 51 | "mocha": "^11.0.1", 52 | "semantic-release": "^22.0.12", 53 | "standard": "^17.0.0" 54 | }, 55 | "engines": { 56 | "node": ">=18" 57 | }, 58 | "files": [ 59 | "lib/", 60 | "bin/", 61 | "tedious.js", 62 | "msnodesqlv8.js" 63 | ], 64 | "scripts": { 65 | "commitlint": "commitlint --from origin/master --to HEAD", 66 | "test": "npm run lint && npm run test-unit", 67 | "lint": "standard", 68 | "test-unit": "mocha --exit -t 15000 test/common/unit.js", 69 | "test-tedious": "mocha --exit -t 15000 test/tedious", 70 | "test-msnodesqlv8": "mocha --exit -t 30000 test/msnodesqlv8", 71 | "test-cli": "mocha --exit -t 15000 test/common/cli.js" 72 | }, 73 | "bin": { 74 | "mssql": "bin/mssql" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tedious.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./lib/tedious') 4 | -------------------------------------------------------------------------------- /test/cleanup.sql: -------------------------------------------------------------------------------- 1 | if exists (select * from sys.procedures where name = '__test') 2 | exec('drop procedure [dbo].[__test]') 3 | 4 | if exists (select * from sys.procedures where name = '__test2') 5 | exec('drop procedure [dbo].[__test2]') 6 | 7 | if exists (select * from sys.procedures where name = '__test3') 8 | exec('drop procedure [dbo].[__test3]') 9 | 10 | if exists (select * from sys.procedures where name = '__test5') 11 | exec('drop procedure [dbo].[__test5]') 12 | 13 | if exists (select * from sys.procedures where name = '__test7') 14 | exec('drop procedure [dbo].[__test7]') 15 | 16 | if exists (select * from sys.procedures where name = '__testDuplicateNames') 17 | exec('drop procedure [dbo].[__testDuplicateNames]') 18 | 19 | if exists (select * from sys.procedures where name = '__testInputOutputValue') 20 | exec('drop procedure [dbo].[__testInputOutputValue]') 21 | 22 | if exists (select * from sys.procedures where name = '__testRowsAffected') 23 | exec('drop procedure [dbo].[__testRowsAffected]') 24 | 25 | if exists (select * from sys.types where is_user_defined = 1 and name = 'MSSQLTestType') 26 | exec('drop type [dbo].[MSSQLTestType]') 27 | 28 | if exists (select * from sys.tables where name = 'prepstm_test') 29 | exec('drop table [dbo].[tvp_test]') 30 | 31 | if exists (select * from sys.tables where name = 'prepstm_test') 32 | exec('drop table [dbo].[prepstm_test]') 33 | 34 | if exists (select * from sys.tables where name = 'tran_test') 35 | exec('drop table [dbo].[tran_test]') 36 | 37 | if exists (select * from sys.tables where name = 'bulk_table') 38 | exec('drop table [dbo].[bulk_table]') 39 | 40 | if exists (select * from sys.tables where name = 'bulk_table2') 41 | exec('drop table [dbo].[bulk_table2]') 42 | 43 | if exists (select * from sys.tables where name = 'bulk_table3') 44 | exec('drop table [dbo].[bulk_table3]') 45 | 46 | if exists (select * from sys.tables where name = 'bulk_table4') 47 | exec('drop table [dbo].[bulk_table4]') 48 | 49 | if exists (select * from sys.tables where name = 'bulk_table5') 50 | exec('drop table [dbo].[bulk_table5]') 51 | 52 | if exists (select * from sys.tables where name = 'rowsaffected_test') 53 | exec('drop table [dbo].[rowsaffected_test]') 54 | 55 | if exists (select * from sys.tables where name = 'streaming') 56 | exec('drop table [dbo].[streaming]') 57 | 58 | -------------------------------------------------------------------------------- /test/common/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* globals describe, it */ 4 | 5 | const assert = require('node:assert') 6 | const { join } = require('node:path') 7 | const { spawn } = require('child_process') 8 | 9 | const config = function () { 10 | const cfg = JSON.parse(require('node:fs').readFileSync(join(__dirname, '../.mssql.json'))) 11 | cfg.driver = 'tedious' 12 | return cfg 13 | } 14 | 15 | function quote (string) { 16 | return `"${string}"` 17 | } 18 | 19 | function cli (args, cwd) { 20 | const isWin = process.platform === 'win32' 21 | let program = join(__dirname, '../../bin/mssql') 22 | if (isWin) { 23 | args.unshift(program) 24 | program = quote(process.argv0) 25 | } 26 | return spawn(program, isWin ? args.map(quote) : args, { cwd, stdio: 'pipe', shell: isWin }) 27 | } 28 | 29 | describe('cli', function () { 30 | it('should stream statement result', (done) => { 31 | const buffer = [] 32 | const proc = cli(['.'], join(__dirname, '..')) 33 | proc.stdin.end('select 1 as xxx') 34 | proc.stdout.setEncoding('utf8') 35 | proc.stdout.on('data', data => buffer.push(data)) 36 | 37 | proc.on('close', function (code) { 38 | assert.strictEqual(code, 0) 39 | assert.strictEqual('[[{"xxx":1}]]\n', buffer.join('')) 40 | done() 41 | }) 42 | }) 43 | 44 | it('fails with no config file', (done) => { 45 | const buffer = [] 46 | const proc = cli(['..'], join(__dirname, '..')) 47 | proc.stdin.end('select 1 as xxx') 48 | proc.stderr.setEncoding('utf8') 49 | proc.stderr.on('data', data => buffer.push(data)) 50 | 51 | proc.on('close', function (code) { 52 | assert.strictEqual(code, 1) 53 | done() 54 | }) 55 | }) 56 | 57 | it('accepts arguments when there is no mssql config file', (done) => { 58 | const cfg = config() 59 | const args = [] 60 | if (cfg.user) { 61 | args.push('--user', cfg.user) 62 | } 63 | if (cfg.password) { 64 | args.push('--password', cfg.password) 65 | } 66 | if (cfg.server) { 67 | args.push('--server', cfg.server) 68 | } 69 | if (cfg.database) { 70 | args.push('--database', cfg.database) 71 | } 72 | if (cfg.port) { 73 | args.push('--port', cfg.port) 74 | } 75 | if (cfg.options.encrypt) { 76 | args.push('--encrypt') 77 | } 78 | if (cfg.options.trustServerCertificate) { 79 | args.push('--trust-server-certificate') 80 | } 81 | args.push('..') 82 | const buffer = [] 83 | const proc = cli(args, join(__dirname, '..')) 84 | proc.stdin.end('select 1 as xxx') 85 | proc.stdout.setEncoding('utf8') 86 | proc.stdout.on('data', data => buffer.push(data)) 87 | 88 | proc.on('close', function (code) { 89 | assert.strictEqual(code, 0) 90 | assert.strictEqual('[[{"xxx":1}]]\n', buffer.join('')) 91 | done() 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/common/templatestring.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | 5 | module.exports = (sql, driver) => { 6 | return { 7 | 'query' (done) { 8 | sql.query`select getdate() as date\n\n, ${1337} as num, ${true} as bool`.then(result => { 9 | assert.ok(result.recordset[0].date instanceof Date) 10 | assert.strictEqual(result.recordset[0].num, 1337) 11 | assert.strictEqual(result.recordset[0].bool, true) 12 | 13 | done() 14 | }).catch(done) 15 | }, 16 | 17 | 'batch' (done) { 18 | sql.batch`select newid() as uid`.then(result => { 19 | assert.strictEqual(result.recordset.columns.uid.type, sql.UniqueIdentifier) 20 | 21 | done() 22 | }).catch(done) 23 | }, 24 | 25 | 'array params' (done) { 26 | const values = [1, 2, 3] 27 | sql.query`select 1 as col where 1 in (${values});`.then(result => { 28 | assert.strictEqual(result.recordset[0].col, 1) 29 | 30 | done() 31 | }) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/common/times.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | 5 | module.exports = (sql, driver) => { 6 | return { 7 | 'time' (utc, done) { 8 | const req = new sql.Request() 9 | req.query("declare @t time(1) = null;select convert(time(0), '23:59:59.999999999') as t1, convert(time(4), '23:59:59.999999999') as t2, convert(time(7), '23:59:59.999999999') as t3, @t as t4").then(result => { 10 | if (utc) { 11 | assert.strictEqual(+result.recordset[0].t1, new Date(Date.UTC(1970, 0, 1, 23, 59, 59)).getTime()) 12 | assert.strictEqual(+result.recordset[0].t2, new Date(Date.UTC(1970, 0, 1, 23, 59, 59, 999)).getTime()) 13 | assert.strictEqual(+result.recordset[0].t3, new Date(Date.UTC(1970, 0, 1, 23, 59, 59, 999)).getTime()) 14 | } else { 15 | assert.strictEqual(+result.recordset[0].t1, new Date(1970, 0, 1, 23, 59, 59).getTime()) 16 | assert.strictEqual(+result.recordset[0].t2, new Date(1970, 0, 1, 23, 59, 59, 999).getTime()) 17 | assert.strictEqual(+result.recordset[0].t3, new Date(1970, 0, 1, 23, 59, 59, 999).getTime()) 18 | } 19 | 20 | assert.strictEqual(result.recordset[0].t4, null) 21 | assert.strictEqual(result.recordset[0].t1.nanosecondsDelta, 0) 22 | assert.strictEqual(result.recordset[0].t2.nanosecondsDelta, 0.0009) 23 | assert.strictEqual(result.recordset[0].t3.nanosecondsDelta, 0.0009999) 24 | 25 | if (driver === 'tedious') { 26 | assert.strictEqual(result.recordset.columns.t1.scale, 0) 27 | assert.strictEqual(result.recordset.columns.t2.scale, 4) 28 | assert.strictEqual(result.recordset.columns.t3.scale, 7) 29 | assert.strictEqual(result.recordset.columns.t4.scale, 1) 30 | } 31 | 32 | done() 33 | }).catch(done) 34 | }, 35 | 36 | 'time as parameter' (utc, done) { 37 | let time 38 | if (utc) { 39 | time = new Date(Date.UTC(2014, 0, 1, 23, 59, 59, 999)) 40 | } else { 41 | time = new Date(2014, 0, 1, 23, 59, 59, 999) 42 | } 43 | 44 | time.nanosecondDelta = 0.0009999 45 | 46 | const req = new sql.Request() 47 | req.input('t1', sql.Time, time) 48 | req.input('t2', sql.Time, null) 49 | req.query('select @t1 as t1, @t2 as t2, convert(varchar, @t1, 126) as t3').then(result => { 50 | if (utc) { 51 | assert.strictEqual(+result.recordset[0].t1, new Date(Date.UTC(1970, 0, 1, 23, 59, 59, 999)).getTime()) 52 | } else { 53 | assert.strictEqual(+result.recordset[0].t1, new Date(1970, 0, 1, 23, 59, 59, 999).getTime()) 54 | } 55 | 56 | assert.strictEqual(result.recordset[0].t2, null) 57 | 58 | if (driver === 'tedious') { 59 | assert.strictEqual(result.recordset[0].t3, '23:59:59.9999999') 60 | assert.strictEqual(result.recordset[0].t1.nanosecondsDelta, 0.0009999) // msnodesql cant pass nanoseconds 61 | assert.strictEqual(result.recordset.columns.t1.scale, 7) 62 | } 63 | 64 | done() 65 | }).catch(done) 66 | }, 67 | 68 | 'date' (utc, done) { 69 | const req = new sql.Request() 70 | req.query("select convert(date, '2014-01-01') as d1").then(result => { 71 | if (utc) { 72 | assert.strictEqual(+result.recordset[0].d1, new Date(Date.UTC(2014, 0, 1)).getTime()) 73 | } else { 74 | assert.strictEqual(+result.recordset[0].d1, new Date(2014, 0, 1).getTime()) 75 | } 76 | 77 | done() 78 | }).catch(done) 79 | }, 80 | 81 | 'date as parameter' (utc, done) { 82 | let date 83 | if (utc) { 84 | date = new Date(Date.UTC(2014, 1, 14)) 85 | } else { 86 | date = new Date(2014, 1, 14) 87 | } 88 | 89 | const req = new sql.Request() 90 | req.input('d1', sql.Date, date) 91 | req.input('d2', sql.Date, null) 92 | req.query('select @d1 as d1, @d2 as d2, convert(varchar, @d1, 126) as d3').then(result => { 93 | if (utc) { 94 | assert.strictEqual(+result.recordset[0].d1, new Date(Date.UTC(2014, 1, 14)).getTime()) 95 | } else { 96 | assert.strictEqual(+result.recordset[0].d1, new Date(2014, 1, 14).getTime()) 97 | } 98 | 99 | assert.strictEqual(result.recordset[0].d2, null) 100 | assert.strictEqual(result.recordset[0].d3, '2014-02-14') 101 | 102 | done() 103 | }).catch(done) 104 | }, 105 | 106 | 'datetime' (utc, done) { 107 | const req = new sql.Request() 108 | req.query("select convert(datetime, '2014-02-14 22:59:59') as dt1").then(result => { 109 | if (utc) { 110 | assert.strictEqual(+result.recordset[0].dt1, new Date(Date.UTC(2014, 1, 14, 22, 59, 59)).getTime()) 111 | } else { 112 | assert.strictEqual(+result.recordset[0].dt1, new Date(2014, 1, 14, 22, 59, 59).getTime()) 113 | } 114 | 115 | done() 116 | }).catch(done) 117 | }, 118 | 119 | 'datetime as parameter' (utc, done) { 120 | const date = new Date(Date.UTC(2014, 1, 14, 22, 59, 59)) 121 | 122 | const req = new sql.Request() 123 | req.input('dt1', sql.DateTime, date) 124 | req.input('dt2', sql.DateTime, null) 125 | req.query('select @dt1 as dt1, @dt2 as dt2').then(result => { 126 | assert.strictEqual(+result.recordset[0].dt1, date.getTime()) 127 | assert.strictEqual(result.recordset[0].dt2, null) 128 | 129 | done() 130 | }).catch(done) 131 | }, 132 | 133 | 'datetime2' (utc, done) { 134 | const req = new sql.Request() 135 | req.query("select convert(datetime2(7), '1111-02-14 22:59:59.9999999') as dt1").then(result => { 136 | if (utc) { 137 | assert.strictEqual(+result.recordset[0].dt1, new Date(Date.UTC(1111, 1, 14, 22, 59, 59, 999)).getTime()) 138 | } else { 139 | assert.strictEqual(+result.recordset[0].dt1, new Date(1111, 1, 14, 22, 59, 59, 999).getTime()) 140 | } 141 | 142 | assert.strictEqual(result.recordset[0].dt1.nanosecondsDelta, 0.0009999) 143 | 144 | if (driver === 'tedious') { 145 | assert.strictEqual(result.recordset.columns.dt1.scale, 7) 146 | } 147 | 148 | done() 149 | }).catch(done) 150 | }, 151 | 152 | 'datetime2 as parameter' (utc, done) { 153 | const date = new Date(2014, 1, 14, 22, 59, 59, 999) 154 | date.nanosecondDelta = 0.0009999 155 | 156 | const req = new sql.Request() 157 | req.input('dt1', sql.DateTime2, date) 158 | req.input('dt2', sql.DateTime2, null) 159 | req.query('select @dt1 as dt1, @dt2 as dt2, convert(varchar, @dt1, 126) as dt3').then(result => { 160 | assert.strictEqual(+result.recordset[0].dt1, date.getTime()) 161 | assert.strictEqual(result.recordset[0].dt2, null) 162 | 163 | if (driver === 'tedious') { 164 | assert.strictEqual(result.recordset[0].dt1.nanosecondsDelta, 0.0009999) // msnodesql cant pass nanoseconds 165 | assert.strictEqual(result.recordset.columns.dt1.scale, 7) 166 | 167 | if (utc) { 168 | assert.strictEqual(result.recordset[0].dt3, date.toISOString().replace('Z', 9999)) 169 | } else { 170 | assert.strictEqual(result.recordset[0].dt3, '2014-02-14T22:59:59.9999999') 171 | } 172 | } 173 | 174 | done() 175 | }).catch(done) 176 | }, 177 | 178 | 'datetimeoffset' (utc, done) { 179 | const req = new sql.Request() 180 | req.query("select convert(datetimeoffset(7), '2014-02-14 22:59:59.9999999 +05:00') as dto1, convert(datetimeoffset(7), '2014-02-14 17:59:59.9999999 +00:00') as dto2").then(result => { 181 | // console.log result.recordset[0] 182 | // console.log new Date(Date.UTC(2014, 1, 14, 22, 59, 59, 999)) 183 | 184 | assert.strictEqual(+result.recordset[0].dto1, new Date(Date.UTC(2014, 1, 14, 17, 59, 59, 999)).getTime()) 185 | assert.strictEqual(+result.recordset[0].dto2, new Date(Date.UTC(2014, 1, 14, 17, 59, 59, 999)).getTime()) 186 | assert.strictEqual(result.recordset[0].dto1.nanosecondsDelta, 0.0009999) // msnodesql cant pass nanoseconds 187 | 188 | if (driver === 'tedious') { 189 | assert.strictEqual(result.recordset.columns.dto1.scale, 7) 190 | assert.strictEqual(result.recordset.columns.dto2.scale, 7) 191 | } 192 | 193 | done() 194 | }).catch(done) 195 | }, 196 | 197 | 'datetimeoffset as parameter' (utc, done) { 198 | const req = new sql.Request() 199 | req.input('dto1', sql.DateTimeOffset, new Date(2014, 1, 14, 11, 59, 59)) 200 | req.input('dto2', sql.DateTimeOffset, new Date(Date.UTC(2014, 1, 14, 11, 59, 59))) 201 | req.input('dto3', sql.DateTimeOffset, null) 202 | req.query('select @dto1 as dto1, @dto2 as dto2, @dto3 as dto3').then(result => { 203 | assert.strictEqual(+result.recordset[0].dto1, new Date(2014, 1, 14, 11, 59, 59).getTime()) 204 | assert.strictEqual(+result.recordset[0].dto2, new Date(Date.UTC(2014, 1, 14, 11, 59, 59)).getTime()) 205 | assert.strictEqual(result.recordset[0].dto3, null) 206 | 207 | if (driver === 'tedious') { 208 | assert.strictEqual(result.recordset.columns.dto1.scale, 7) 209 | } 210 | 211 | done() 212 | }).catch(done) 213 | }, 214 | 215 | 'smalldatetime' (utc, done) { 216 | const req = new sql.Request() 217 | req.query("select convert(datetime, '2014-02-14 22:59:59') as dt1").then(result => { 218 | if (utc) { 219 | assert.strictEqual(+result.recordset[0].dt1, new Date(Date.UTC(2014, 1, 14, 22, 59, 59)).getTime()) 220 | } else { 221 | assert.strictEqual(+result.recordset[0].dt1, new Date(2014, 1, 14, 22, 59, 59).getTime()) 222 | } 223 | 224 | done() 225 | }).catch(done) 226 | }, 227 | 228 | 'smalldatetime as parameter' (utc, done) { 229 | const date = new Date(2014, 1, 14, 22, 59) 230 | 231 | const req = new sql.Request() 232 | req.input('dt1', sql.SmallDateTime, date) 233 | req.input('dt2', sql.SmallDateTime, null) 234 | req.query('select @dt1 as dt1, @dt2 as dt2').then(result => { 235 | assert.strictEqual(+result.recordset[0].dt1, date.getTime()) 236 | assert.strictEqual(result.recordset[0].dt2, null) 237 | 238 | done() 239 | }).catch(done) 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /test/common/versionhelper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | 'getSQLServerVersion' (sql) { 5 | const req = new sql.Request() 6 | return req.query("select SERVERPROPERTY('productversion') as version").then(result => { 7 | return result.recordset[0].version 8 | }) 9 | }, 10 | 11 | 'isSQLServer2016OrNewer' (sql) { 12 | return this.getSQLServerVersion(sql).then(version => { 13 | const majorVersion = parseInt(version) 14 | return majorVersion >= 13 15 | }) 16 | }, 17 | 18 | 'isSQLServer2019OrNewer' (sql) { 19 | return this.getSQLServerVersion(sql).then(version => { 20 | const majorVersion = parseInt(version) 21 | return majorVersion >= 15 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 2 | --reporter spec 3 | --recursive 4 | -------------------------------------------------------------------------------- /test/msnodesqlv8/msnodesqlv8.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* globals describe, it, before, after, afterEach */ 4 | 5 | const { join } = require('node:path') 6 | const sql = require('../../msnodesqlv8') 7 | 8 | const TESTS = require('../common/tests.js')(sql, 'msnodesqlv8') 9 | const TIMES = require('../common/times.js')(sql, 'msnodesqlv8') 10 | const versionHelper = require('../common/versionhelper') 11 | 12 | const config = function () { 13 | const cfg = JSON.parse(require('node:fs').readFileSync(join(__dirname, '../.mssql.json'))) 14 | cfg.driver = 'msnodesqlv8' 15 | return cfg 16 | } 17 | 18 | let connection1 = null 19 | let connection2 = null 20 | 21 | describe('msnodesqlv8', function () { 22 | before(done => 23 | sql.connect(config(), function (err) { 24 | if (err) return done(err) 25 | 26 | let req = new sql.Request() 27 | req.batch(require('node:fs').readFileSync(join(__dirname, '../cleanup.sql'), 'utf8'), function (err) { 28 | if (err) return done(err) 29 | 30 | req = new sql.Request() 31 | req.batch(require('node:fs').readFileSync(join(__dirname, '../prepare.sql'), 'utf8'), function (err) { 32 | if (err) return done(err) 33 | 34 | sql.close(done) 35 | }) 36 | }) 37 | }) 38 | ) 39 | afterEach(() => sql.valueHandler.clear()) 40 | 41 | describe('basic test suite', function () { 42 | before(function (done) { 43 | const cfg = config() 44 | sql.connect(cfg, done) 45 | }) 46 | 47 | it('config validation', done => TESTS['config validation'](done)) 48 | it('value handler', done => TESTS['value handler'](done)) 49 | it('bigint inputs', done => TESTS['bigint inputs'](done)) 50 | it('stored procedure (exec)', done => TESTS['stored procedure']('execute', done)) 51 | it('stored procedure (batch)', done => TESTS['stored procedure']('batch', done)) 52 | it('user defined types', done => TESTS['user defined types'](done)) 53 | it('binary data', done => TESTS['binary data'](done)) 54 | it('variant data', done => TESTS['variant data'](done)) 55 | it('stored procedure with one empty recordset', done => TESTS['stored procedure with one empty recordset'](done)) 56 | it('stored procedure with duplicate output column names', done => TESTS['stored procedure with duplicate output column names'](done)) 57 | it('stored procedure with input/output column', done => TESTS['stored procedure with input/output column'](done)) 58 | it('empty query', done => TESTS['empty query'](done)) 59 | it('query with no recordset', done => TESTS['query with no recordset'](done)) 60 | it('query with one recordset', done => TESTS['query with one recordset'](done)) 61 | it('query with multiple recordsets', done => TESTS['query with multiple recordsets'](done)) 62 | it('query with input parameters', done => TESTS['query with input parameters']('query', done)) 63 | it('query with input parameters (batch)', done => TESTS['query with input parameters']('batch', done)) 64 | it('query with output parameters', done => TESTS['query with output parameters']('query', done)) 65 | it('query with output parameters (batch)', done => TESTS['query with output parameters']('batch', done)) 66 | it('query with duplicate parameters throws', done => TESTS['query with duplicate parameters throws'](done)) 67 | it('query parameters can be replaced', done => TESTS['query parameters can be replaced'](done)) 68 | it('query with error', done => TESTS['query with error'](done)) 69 | it('query with multiple errors (not supported by msnodesqlv8)', done => TESTS['query with multiple errors'](done)) 70 | it.skip('query with raiseerror (not supported by msnodesqlv8)', done => TESTS['query with raiseerror'](done)) 71 | it('query with toReadableStream', done => TESTS['query with toReadableStream'](done)) 72 | it('query with pipe', done => TESTS['query with pipe'](done)) 73 | it('query with pipe and back pressure', (done) => TESTS['query with pipe and back pressure'](done)) 74 | it('batch', done => TESTS.batch(done)) 75 | it('batch (stream)', done => TESTS.batch(done, true)) 76 | it('create procedure batch', done => TESTS['create procedure batch'](done)) 77 | it('prepared statement', done => TESTS['prepared statement'](done)) 78 | it('prepared statement that fails to prepare throws', done => TESTS['prepared statement that fails to prepare throws'](done)) 79 | it('prepared statement with duplicate parameters throws', done => TESTS['prepared statement with duplicate parameters throws'](done)) 80 | it('prepared statement parameters can be replaced', done => TESTS['prepared statement parameters can be replaced'](done)) 81 | it('prepared statement with affected rows', done => TESTS['prepared statement with affected rows'](done)) 82 | it('prepared statement in transaction', done => TESTS['prepared statement in transaction'](done)) 83 | it('prepared statement with duplicate output column names', done => TESTS['prepared statement with duplicate output column names'](done)) 84 | it('transaction with rollback', done => TESTS['transaction with rollback'](done)) 85 | it('transaction with commit', done => TESTS['transaction with commit'](done)) 86 | it('transaction throws on bad isolation level', done => TESTS['transaction throws on bad isolation level'](done)) 87 | it('transaction accepts good isolation levels', done => TESTS['transaction accepts good isolation levels'](done)) 88 | it('transaction uses default isolation level', done => TESTS['transaction uses default isolation level'](done)) 89 | it('cancel request', done => TESTS['cancel request'](done)) 90 | it('allows repeat calls to connect', done => TESTS['repeat calls to connect resolve'](config(), done)) 91 | it('calls to close during connection throw', done => TESTS['calls to close during connection throw'](config(), done)) 92 | it('connection healthy works', done => TESTS['connection healthy works'](config(), done)) 93 | it('healthy connection goes bad', done => TESTS['healthy connection goes bad'](config(), done)) 94 | it('request timeout', done => TESTS['request timeout'](done)) 95 | it('dataLength type correction', done => TESTS['dataLength type correction'](done)) 96 | it('chunked xml support', done => TESTS['chunked xml support'](done)) 97 | 98 | after(() => sql.close()) 99 | }) 100 | 101 | describe('global connection', () => { 102 | it('repeat calls to connect resolve in order', done => TESTS['repeat calls to connect resolve in order'](sql.connect.bind(sql, config()), done)) 103 | afterEach(done => sql.close(done)) 104 | }) 105 | 106 | describe('json support (requires SQL Server 2016 or newer)', () => { 107 | before(function (done) { 108 | const cfg = config() 109 | cfg.parseJSON = true 110 | sql.connect(cfg) 111 | .then(() => versionHelper.isSQLServer2016OrNewer(sql)).then(isSQLServer2016OrNewer => { 112 | if (!isSQLServer2016OrNewer) { 113 | this.skip() 114 | } 115 | done() 116 | }).catch(done) 117 | }) 118 | 119 | it('parser', done => TESTS['json parser'](done)) 120 | it('empty json', done => TESTS['empty json'](done)) 121 | it('chunked json support', done => TESTS['chunked json support'](done)) 122 | 123 | after(done => sql.close(done)) 124 | }) 125 | 126 | describe('bulk load', function () { 127 | before(function (done) { 128 | sql.connect(config(), function (err) { 129 | if (err) return done(err) 130 | 131 | const req = new sql.Request() 132 | req.query('delete from bulk_table', done) 133 | }) 134 | }) 135 | 136 | it('bulk load (table)', done => TESTS['bulk load']('bulk_table', done)) 137 | it('bulk load (temporary table)', done => TESTS['bulk load']('#anohter_bulk_table', done)) 138 | it('bulk converts dates', done => TESTS['bulk converts dates'](done)) 139 | 140 | after(done => sql.close(done)) 141 | }) 142 | 143 | describe('msnodesqlv8 dates and times', function () { 144 | before(function (done) { 145 | sql.connect(config(), done) 146 | }) 147 | 148 | it('time', done => TIMES.time(true, done)) 149 | it('time as parameter', done => TIMES['time as parameter'](true, done)) 150 | it('date', done => TIMES.date(true, done)) 151 | it('date as parameter', done => TIMES['date as parameter'](true, done)) 152 | it('datetime', done => TIMES.datetime(true, done)) 153 | it('datetime as parameter', done => TIMES['datetime as parameter'](true, done)) 154 | it('datetime2', done => TIMES.datetime2(true, done)) 155 | it('datetime2 as parameter', done => TIMES['datetime2 as parameter'](true, done)) 156 | it('datetimeoffset', done => TIMES.datetimeoffset(true, done))// https://github.com/WindowsAzure/node-sqlserver/issues/160 157 | it('datetimeoffset as parameter', done => TIMES['datetimeoffset as parameter'](true, done)) // https://github.com/WindowsAzure/node-sqlserver/issues/160 158 | it('smalldatetime', done => TIMES.smalldatetime(true, done)) 159 | it('smalldatetime as parameter', done => TIMES['smalldatetime as parameter'](true, done)) 160 | 161 | after(() => sql.close()) 162 | }) 163 | 164 | describe('msnodesqlv8 multiple connections test suite', function () { 165 | before(function (done) { 166 | global.SPIDS = {} 167 | connection1 = new sql.ConnectionPool(config(), () => { 168 | connection2 = new sql.ConnectionPool(config(), () => sql.connect(config(), done)) 169 | }) 170 | }) 171 | 172 | it('connection 1', done => TESTS['connection 1'](done, connection1)) 173 | it('connection 2', done => TESTS['connection 2'](done, connection2)) 174 | it('global connection', done => TESTS['global connection'](done)) 175 | 176 | after(function () { 177 | connection1.close() 178 | connection2.close() 179 | sql.close() 180 | }) 181 | }) 182 | 183 | describe('msnodesqlv8 connection errors', function () { 184 | it('login failed', done => TESTS['login failed'](done, /Login failed for user '(.*)'\./)) 185 | it.skip('timeout (not supported by msnodesqlv8)', done => TESTS.timeout.call(this, done, /1000ms/)) 186 | it.skip('network error (not supported by msnodesqlv8)', done => TESTS['network error'](done)) 187 | }) 188 | 189 | describe('msnodesqlv8 connection pooling', function () { 190 | before(done => { 191 | connection1 = new sql.ConnectionPool(config(), function () { 192 | const cfg = config() 193 | cfg.pool = { max: 1 } 194 | connection2 = new sql.ConnectionPool(cfg, done) 195 | }) 196 | }) 197 | 198 | it('max 10', done => TESTS['max 10'](done, connection1)) 199 | it('max 1', done => TESTS['max 1'](done, connection2)) 200 | it.skip('interruption (not supported by msnodesqlv8)', done => TESTS.interruption(done, connection1, connection2)) 201 | 202 | after(function () { 203 | connection1.close() 204 | connection2.close() 205 | }) 206 | }) 207 | 208 | describe('msnodesqlv8 stress', function () { 209 | before((done) => { 210 | const cfg = config() 211 | cfg.options.abortTransactionOnError = true 212 | // cfg.requestTimeout = 60000 213 | sql.connect(cfg, done) 214 | }) 215 | 216 | it.skip('concurrent connections', done => TESTS['concurrent connections'](done)) 217 | it.skip('concurrent requests', done => TESTS['concurrent requests'](done)) 218 | it('streaming off', done => TESTS['streaming off'](done)) 219 | it('streaming on', done => TESTS['streaming on'](done)) 220 | it('streaming pause', done => TESTS['streaming pause'](done)) 221 | it('streaming resume', done => TESTS['streaming resume'](done)) 222 | it('streaming rowsaffected', done => TESTS['streaming rowsaffected'](done)) 223 | it('streaming rowsaffected in stored procedure', done => TESTS['streaming rowsaffected in stored procedure'](done)) 224 | it('streaming trailing rows', done => TESTS['streaming trailing rows'](done)) 225 | it('streaming with duplicate output column names', done => TESTS['streaming with duplicate output column names'](done)) 226 | it('a cancelled stream emits done event', done => TESTS['a cancelled stream emits done event'](done)) 227 | it('a cancelled paused stream emits done event', done => TESTS['a cancelled paused stream emits done event'](done)) 228 | 229 | after(done => sql.close(done)) 230 | }) 231 | 232 | describe('tvp', function () { 233 | before((done) => { 234 | sql.connect(config(), done) 235 | }) 236 | 237 | it('new Table', done => TESTS['new Table'](done)) 238 | it('Recordset.toTable()', done => TESTS['Recordset.toTable()'](done)) 239 | it('Recordset.toTable() from existing', done => TESTS['Recordset.toTable() from existing'](done)) 240 | 241 | after(() => sql.close()) 242 | }) 243 | 244 | after('cleanup', done => 245 | sql.connect(config(), function (err) { 246 | if (err) return done(err) 247 | 248 | const req = new sql.Request() 249 | req.query(require('node:fs').readFileSync(join(__dirname, '../cleanup.sql'), 'utf8'), function (err) { 250 | if (err) return done(err) 251 | 252 | sql.close(done) 253 | }) 254 | }) 255 | ) 256 | }) 257 | -------------------------------------------------------------------------------- /test/prepare.sql: -------------------------------------------------------------------------------- 1 | exec('create procedure [dbo].[__test] 2 | @in int, 3 | @in2 int, 4 | @in3 varchar (10), 5 | @in4 uniqueidentifier = null, 6 | @in5 datetime = null, 7 | @out int output, 8 | @out2 int output, 9 | @out3 uniqueidentifier = null output, 10 | @out4 datetime = null output, 11 | @out5 char(10) = null output 12 | as 13 | begin 14 | 15 | set nocount on; 16 | 17 | declare @table table (a int, b int) 18 | insert into @table values (1, 2) 19 | insert into @table values (3, 4) 20 | 21 | select * from @table 22 | 23 | select 5 as ''c'', 6 as ''d'', @in2 as ''e'', 111 as ''e'', ''asdf'' as ''e'', null as ''f'', @in3 as ''g'' 24 | 25 | select * from @table where a = 11 26 | 27 | set @out = 99 28 | set @out2 = @in 29 | set @out3 = @in4 30 | set @out4 = @in5 31 | set @out5 = @in3 32 | 33 | return 11 34 | 35 | end') 36 | 37 | exec('create procedure [dbo].[__test2] 38 | as 39 | begin 40 | 41 | set nocount on 42 | 43 | declare @table table (a int, b int) 44 | select * from @table 45 | 46 | select ''asdf'' as string 47 | 48 | return 11 49 | 50 | end') 51 | 52 | exec('create procedure [dbo].[__test3] 53 | as 54 | begin 55 | 56 | with n(n) as (select 1 union all select n +1 from n where n < 1000) select n from n order by n option (maxrecursion 1000) for xml auto; 57 | 58 | end') 59 | 60 | exec('create procedure [dbo].[__test5] 61 | @in BINARY(4), 62 | @in2 BINARY(4) = NULL, 63 | @in3 VARBINARY(MAX), 64 | @in4 VARBINARY(MAX) = NULL, 65 | @in5 IMAGE, 66 | @in6 IMAGE = NULL, 67 | @out BINARY(4) = NULL OUTPUT, 68 | @out2 VARBINARY(MAX) = NULL OUTPUT 69 | as 70 | begin 71 | 72 | set nocount on 73 | 74 | select CAST( 123456 AS BINARY(4) ) as ''bin'', @in as ''in'', @in2 as ''in2'', @in3 as ''in3'', @in4 as ''in4'', @in5 as ''in5'', @in6 as ''in6'' 75 | 76 | set @out = @in 77 | set @out2 = @in3 78 | 79 | return 0 80 | 81 | end') 82 | 83 | exec('create procedure [dbo].[__testDuplicateNames] 84 | @in int, 85 | @in2 int, 86 | @out int = NULL OUTPUT, 87 | @out2 int = NULL OUTPUT 88 | as 89 | begin 90 | 91 | set nocount on 92 | 93 | select @in as ''in_value'', @in2 as ''in_value'' 94 | 95 | set @out = @in2 96 | set @out2 = @in 97 | 98 | return 12 99 | 100 | end') 101 | 102 | exec('create procedure [dbo].[__testInputOutputValue] 103 | @in int, 104 | @out int = NULL OUTPUT 105 | as 106 | begin 107 | set @out = @out + @in 108 | end') 109 | 110 | exec('create type [dbo].[MSSQLTestType] as table( 111 | [a] [varchar](50) null, 112 | [b] [integer] null 113 | )') 114 | 115 | exec('create procedure [dbo].[__test7] 116 | @tvp MSSQLTestType readonly 117 | as 118 | begin 119 | 120 | select * from @tvp 121 | 122 | end') 123 | 124 | exec('create table [dbo].[tvp_test] ( 125 | a int not null identity, 126 | b varchar(50) null, 127 | c as ''id is '' + cast(a as varchar(10)) persisted 128 | )') 129 | 130 | exec('create table [dbo].[prepstm_test] ( 131 | data varchar(50) not null 132 | )') 133 | 134 | exec('create table [dbo].[tran_test] ( 135 | data varchar(50) not null 136 | )') 137 | 138 | exec('create table [dbo].[bulk_table] ( 139 | a int not null, 140 | b varchar (50) null, 141 | c image null 142 | )') 143 | 144 | exec('create table [dbo].[rowsaffected_test] ( 145 | a int not null 146 | )') 147 | 148 | ;with nums as 149 | ( 150 | select 0 AS n 151 | union all 152 | select n + 1 from nums where n < 6 153 | ) 154 | insert into rowsaffected_test(a) 155 | select n from nums 156 | option (maxrecursion 7); 157 | 158 | exec('create table [dbo].[streaming] ( 159 | text varchar(4000) 160 | )') 161 | 162 | exec('create procedure [dbo].[__testRowsAffected] 163 | as 164 | begin 165 | 166 | update rowsaffected_test set a = a 167 | 168 | end') 169 | 170 | ;with nums as 171 | ( 172 | select 0 AS n 173 | union all 174 | select n + 1 from nums where n < 32767 175 | ) 176 | insert into streaming(text) 177 | select 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras commodo lacinia turpis, et volutpat magna euismod at. Sed eget interdum enim, sed sagittis augue. Donec aliquet lacinia commodo. Nunc ultricies felis ut ante lobortis consectetur. Etiam dictum elit quis eros fermentum, sed venenatis libero elementum. Cras sed luctus eros. Donec ultrices mauris a velit gravida lobortis. Sed at nulla sit amet eros semper viverra. Pellentesque aliquam accumsan ligula, sed euismod est suscipit ut. Etiam facilisis dapibus viverra. In hac habitasse platea dictumst. Quisque lacinia mattis quam, sit amet lacinia felis convallis id. Interdum et malesuada fames ac ante ipsum primis in faucibus. Proin dapibus auctor lacinia. Nam dictum orci at neque adipiscing sollicitudin. Quisque id enim rutrum, tempor arcu ut, tempor mi. Vivamus fringilla velit vel massa fringilla, a interdum felis pellentesque. Etiam faucibus felis nec elit sodales molestie. Quisque sit amet porta nisi. Nunc tellus diam, sagittis eu porta vel, sagittis eu urna. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur quis scelerisque nisl. Nulla egestas blandit felis id condimentum. Sed eleifend neque sit amet nisl vehicula molestie. Nulla ut mi dignissim, faucibus nulla quis, hendrerit neque. Maecenas luctus urna urna, eget placerat metus tempor nec. Aenean accumsan nunc at leo tempus vehicula. In hac habitasse platea dictumst. Vestibulum faucibus scelerisque nisi, et adipiscing justo. Praesent posuere placerat nibh aliquet suscipit. Morbi eget consectetur sem. Nulla erat ipsum, dapibus sit amet nulla in, dictum malesuada felis. Sed eu blandit est. Etiam suscipit lacus elit, quis pretium diam ultricies ac. Sed tincidunt mollis accumsan. Donec scelerisque sapien ac tincidunt eleifend. Quisque nec sem dolor. Suspendisse imperdiet facilisis velit, non faucibus justo consequat elementum. Sed id purus mauris. Nunc id tortor rutrum, ornare leo at, ultrices urna. Nam dolor augue, fermentum sed condimentum et, pulvinar interdum augue. Sed arcu nibh, tincidunt id bibendum ut, placerat eu odio. Phasellus viverra nisi sagittis auctor tristique. Phasellus ullamcorper mauris eget ipsum faucibus accumsan. Mauris non quam orci.' from nums 178 | option (maxrecursion 32767); 179 | -------------------------------------------------------------------------------- /test/tedious/tedious.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* globals describe, it, before, after, afterEach */ 4 | 5 | const sql = require('../../tedious.js') 6 | const assert = require('node:assert') 7 | const { join } = require('node:path') 8 | 9 | const TESTS = require('../common/tests.js')(sql, 'tedious') 10 | const TIMES = require('../common/times.js')(sql, 'tedious') 11 | const TEMPLATE_STRING = require('../common/templatestring.js')(sql, 'tedious') 12 | const versionHelper = require('../common/versionhelper') 13 | 14 | if (parseInt(process.version.match(/^v(\d+)\./)[1]) > 0) { 15 | require('../common/templatestring.js') 16 | } 17 | 18 | const config = function () { 19 | const cfg = JSON.parse(require('node:fs').readFileSync(join(__dirname, '../.mssql.json'))) 20 | cfg.driver = 'tedious' 21 | return cfg 22 | } 23 | 24 | let connection1 = null 25 | let connection2 = null 26 | 27 | describe('tedious', () => { 28 | before(done => 29 | sql.connect(config(), err => { 30 | if (err) return done(err) 31 | 32 | let req = new sql.Request() 33 | req.query(require('node:fs').readFileSync(join(__dirname, '../cleanup.sql'), 'utf8'), err => { 34 | if (err) return done(err) 35 | 36 | req = new sql.Request() 37 | req.query(require('node:fs').readFileSync(join(__dirname, '../prepare.sql'), 'utf8'), err => { 38 | if (err) return done(err) 39 | 40 | sql.close(done) 41 | }) 42 | }) 43 | }) 44 | ) 45 | afterEach(() => sql.valueHandler.clear()) 46 | 47 | describe('basic test suite', () => { 48 | before((done) => { 49 | const cfg = config() 50 | cfg.options.abortTransactionOnError = true 51 | sql.connect(cfg, done) 52 | }) 53 | 54 | it('config validation', done => TESTS['config validation'](done)) 55 | it('value handler', done => TESTS['value handler'](done)) 56 | it('bigint inputs', done => TESTS['bigint inputs'](done)) 57 | it('stored procedure (exec)', done => TESTS['stored procedure']('execute', done)) 58 | it('stored procedure (batch)', done => TESTS['stored procedure']('batch', done)) 59 | it('user defined types', done => TESTS['user defined types'](done)) 60 | it('binary data', done => TESTS['binary data'](done)) 61 | it('variant data (not yet published)', done => TESTS['variant data'](done)) 62 | it('stored procedure with one empty recordset', done => TESTS['stored procedure with one empty recordset'](done)) 63 | it('stored procedure with duplicate output column names', done => TESTS['stored procedure with duplicate output column names'](done)) 64 | it('stored procedure with input/output column', done => TESTS['stored procedure with input/output column'](done)) 65 | it('empty query', done => TESTS['empty query'](done)) 66 | it('query with no recordset', done => TESTS['query with no recordset'](done)) 67 | it('query with one recordset', done => TESTS['query with one recordset'](done)) 68 | it('query with multiple recordsets', done => TESTS['query with multiple recordsets'](done)) 69 | it('query with input parameters', done => TESTS['query with input parameters']('query', done)) 70 | it('query with input parameters (batch)', done => TESTS['query with input parameters']('batch', done)) 71 | it('query with output parameters', done => TESTS['query with output parameters']('query', done)) 72 | it('query with output parameters (batch)', done => TESTS['query with output parameters']('batch', done)) 73 | it('query with duplicate parameters throws', done => TESTS['query with duplicate parameters throws'](done)) 74 | it('query parameters can be replaced', done => TESTS['query parameters can be replaced'](done)) 75 | it('query with error', done => TESTS['query with error'](done)) 76 | it('query with multiple errors', done => TESTS['query with multiple errors'](done)) 77 | it('query with raiseerror', done => TESTS['query with raiseerror'](done)) 78 | it('query with toReadableStream', done => TESTS['query with toReadableStream'](done)) 79 | it('query with pipe', done => TESTS['query with pipe'](done)) 80 | it('query with pipe and back pressure', (done) => TESTS['query with pipe and back pressure'](done)) 81 | it('query with duplicate output column names', done => TESTS['query with duplicate output column names'](done)) 82 | it('batch', done => TESTS.batch(done)) 83 | it('create procedure batch', done => TESTS['create procedure batch'](done)) 84 | it('prepared statement', done => TESTS['prepared statement'](done)) 85 | it('prepared statement that fails to prepare throws', done => TESTS['prepared statement that fails to prepare throws'](done)) 86 | it('prepared statement with duplicate parameters throws', done => TESTS['prepared statement with duplicate parameters throws'](done)) 87 | it('prepared statement parameters can be replaced', done => TESTS['prepared statement parameters can be replaced'](done)) 88 | it('prepared statement with affected rows', done => TESTS['prepared statement with affected rows'](done)) 89 | it('prepared statement in transaction', done => TESTS['prepared statement in transaction'](done)) 90 | it('prepared statement with duplicate output column names', done => TESTS['prepared statement with duplicate output column names'](done)) 91 | it('transaction with rollback', done => TESTS['transaction with rollback'](done)) 92 | it('transaction with commit', done => TESTS['transaction with commit'](done)) 93 | it('transaction throws on bad isolation level', done => TESTS['transaction throws on bad isolation level'](done)) 94 | it('transaction accepts good isolation levels', done => TESTS['transaction accepts good isolation levels'](done)) 95 | it('transaction uses default isolation level', done => TESTS['transaction uses default isolation level'](done)) 96 | it('transaction with error (XACT_ABORT set to ON)', done => TESTS['transaction with error'](done)) 97 | it('transaction with synchronous error', done => TESTS['transaction with synchronous error'](done)) 98 | it('cancel request', done => TESTS['cancel request'](done, /Canceled./)) 99 | it('allows repeat calls to connect', done => TESTS['repeat calls to connect resolve'](config(), done)) 100 | it('calls to close during connection throw', done => TESTS['calls to close during connection throw'](config(), done)) 101 | it('connection healthy works', done => TESTS['connection healthy works'](config(), done)) 102 | it('healthy connection goes bad', done => TESTS['healthy connection goes bad'](config(), done)) 103 | it('request timeout', done => TESTS['request timeout'](done, 'tedious', /Timeout: Request failed to complete in 1000ms/)) 104 | it('dataLength type correction', done => TESTS['dataLength type correction'](done)) 105 | it('type validation', done => TESTS['type validation']('query', done)) 106 | it('type validation (batch)', done => TESTS['type validation']('batch', done)) 107 | it('chunked xml support', done => TESTS['chunked xml support'](done)) 108 | 109 | after(done => sql.close(done)) 110 | }) 111 | 112 | describe('global connection', () => { 113 | it('repeat calls to connect resolve in order', done => TESTS['repeat calls to connect resolve in order'](sql.connect.bind(sql, config()), done)) 114 | afterEach(done => sql.close(done)) 115 | }) 116 | 117 | describe('json support (requires SQL Server 2016 or newer)', () => { 118 | before(function (done) { 119 | const cfg = config() 120 | cfg.parseJSON = true 121 | sql.connect(cfg) 122 | .then(() => versionHelper.isSQLServer2016OrNewer(sql)).then(isSQLServer2016OrNewer => { 123 | if (!isSQLServer2016OrNewer) { 124 | this.skip() 125 | } 126 | done() 127 | }).catch(done) 128 | }) 129 | 130 | it('parser', done => TESTS['json parser'](done)) 131 | it('empty json', done => TESTS['empty json'](done)) 132 | it('chunked json support', done => TESTS['chunked json support'](done)) 133 | 134 | after(done => sql.close(done)) 135 | }) 136 | 137 | describe('bulk load', () => { 138 | before((done) => { 139 | sql.connect(config(), (err) => { 140 | if (err) return done(err) 141 | 142 | const req = new sql.Request() 143 | req.query('delete from bulk_table', done) 144 | }) 145 | }) 146 | 147 | it('bulk load (table)', done => TESTS['bulk load']('bulk_table', done)) 148 | it('bulk load with varchar-max field (table)', done => TESTS['bulk load with varchar-max field']('bulk_table2', done)) 149 | it('bulk load (temporary table)', done => TESTS['bulk load']('#anohter_bulk_table', done)) 150 | it('bulk converts dates', done => TESTS['bulk converts dates'](done)) 151 | it('bulk insert with length option as undefined throws (table)', done => TESTS['bulk insert with length option as undefined throws']('bulk_table3', done)) 152 | it('bulk insert with length option as string other than max throws (table)', done => TESTS['bulk insert with length option as string other than max throws']('bulk_table4', done)) 153 | it('bulk insert with length as max (table)', done => TESTS['bulk insert with length as max']('bulk_table5', done)) 154 | after(done => sql.close(done)) 155 | }) 156 | 157 | describe('dates and times (local)', () => { 158 | before(function (done) { 159 | const cfg = config() 160 | cfg.options.useUTC = false 161 | sql.connect(cfg, done) 162 | }) 163 | 164 | it('time', done => TIMES.time(false, done)) 165 | it('time as parameter', done => TIMES['time as parameter'](false, done)) 166 | it('date', done => TIMES.date(false, done)) 167 | it('date as parameter', done => TIMES['date as parameter'](false, done)) 168 | it('datetime', done => TIMES.datetime(false, done)) 169 | it('datetime as parameter', done => TIMES['datetime as parameter'](false, done)) 170 | it('datetime2', done => TIMES.datetime2(false, done)) 171 | it('datetime2 as parameter', done => TIMES['datetime2 as parameter'](false, done)) 172 | it('datetimeoffset', done => TIMES.datetimeoffset(false, done)) 173 | it('datetimeoffset as parameter', done => TIMES['datetimeoffset as parameter'](false, done)) 174 | it('smalldatetime', done => TIMES.smalldatetime(false, done)) 175 | it('smalldatetime as parameter', done => TIMES['smalldatetime as parameter'](false, done)) 176 | 177 | return after(done => sql.close(done)) 178 | }) 179 | 180 | describe('dates and times (utc)', () => { 181 | before(function (done) { 182 | const cfg = config() 183 | cfg.options.useUTC = true 184 | sql.connect(cfg, done) 185 | }) 186 | 187 | it('time', done => TIMES.time(true, done)) 188 | it('time as parameter', done => TIMES['time as parameter'](true, done)) 189 | it('date', done => TIMES.date(true, done)) 190 | it('date as parameter', done => TIMES['date as parameter'](true, done)) 191 | it('datetime', done => TIMES.datetime(true, done)) 192 | it('datetime as parameter', done => TIMES['datetime as parameter'](true, done)) 193 | it('datetime2', done => TIMES.datetime2(true, done)) 194 | it('datetime2 as parameter', done => TIMES['datetime2 as parameter'](true, done)) 195 | it('datetimeoffset', done => TIMES.datetimeoffset(true, done)) 196 | it('datetimeoffset as parameter', done => TIMES['datetimeoffset as parameter'](true, done)) 197 | it('smalldatetime', done => TIMES.smalldatetime(true, done)) 198 | it('smalldatetime as parameter', done => TIMES['smalldatetime as parameter'](true, done)) 199 | 200 | after(done => sql.close(done)) 201 | }) 202 | 203 | describe('template strings', () => { 204 | before((done) => { 205 | sql.connect(config(), done) 206 | }) 207 | 208 | it('query', done => TEMPLATE_STRING.query(done)) 209 | it('batch', done => TEMPLATE_STRING.batch(done)) 210 | it('array params', done => TEMPLATE_STRING['array params'](done)) 211 | 212 | after(done => sql.close(done)) 213 | }) 214 | 215 | describe('multiple connections test suite', () => { 216 | before((done) => { 217 | global.SPIDS = {} 218 | connection1 = new sql.ConnectionPool(config(), () => { 219 | connection2 = new sql.ConnectionPool(config(), () => sql.connect(config(), done)) 220 | }) 221 | }) 222 | 223 | it('connection 1', done => TESTS['connection 1'](done, connection1)) 224 | it('connection 2', done => TESTS['connection 2'](done, connection2)) 225 | it('global connection', done => TESTS['global connection'](done)) 226 | 227 | after((done) => { 228 | connection1.close() 229 | connection2.close() 230 | sql.close(done) 231 | }) 232 | }) 233 | 234 | describe('connection errors', function () { 235 | it('login failed', done => TESTS['login failed'](done, /Login failed for user '(.*)'/)) 236 | // call(this) to enable the test to skip itself. 237 | it('timeout', function (done) { TESTS.timeout.call(this, done, /Failed to connect to 10.0.0.1:1433 in 1000ms/) }) 238 | it('network error', done => TESTS['network error'](done, /Failed to connect to \.\.\.:1433 - getaddrinfo ENOTFOUND/)) 239 | }) 240 | 241 | describe('connection pooling', () => { 242 | before(done => { 243 | connection1 = new sql.ConnectionPool(config(), () => { 244 | const cfg = config() 245 | cfg.pool = { max: 1 } 246 | connection2 = new sql.ConnectionPool(cfg, done) 247 | }) 248 | }) 249 | 250 | it('max 10', done => TESTS['max 10'](done, connection1)) 251 | it('max 1', done => TESTS['max 1'](done, connection2)) 252 | it('interruption', done => TESTS.interruption(done, connection1, connection2)) 253 | 254 | after(() => { 255 | connection1.close() 256 | connection2.close() 257 | }) 258 | }) 259 | 260 | describe('Stress', function stress () { 261 | before((done) => { 262 | const cfg = config() 263 | cfg.options.abortTransactionOnError = true 264 | // cfg.requestTimeout = 60000 265 | sql.connect(cfg, done) 266 | }) 267 | 268 | it.skip('concurrent connections', done => TESTS['concurrent connections'](done)) 269 | it.skip('concurrent requests', done => TESTS['concurrent requests'](done)) 270 | it('streaming off', done => TESTS['streaming off'](done)) 271 | it('streaming on', done => TESTS['streaming on'](done)) 272 | it('streaming pause', done => TESTS['streaming pause'](done)) 273 | it('streaming resume', done => TESTS['streaming resume'](done)) 274 | it('streaming rowsaffected', done => TESTS['streaming rowsaffected'](done)) 275 | it('streaming rowsaffected in stored procedure', done => TESTS['streaming rowsaffected in stored procedure'](done)) 276 | it('streaming trailing rows', done => TESTS['streaming trailing rows'](done)) 277 | it('streaming with duplicate output column names', done => TESTS['streaming with duplicate output column names'](done)) 278 | it('a cancelled stream emits done event', done => TESTS['a cancelled stream emits done event'](done)) 279 | it('a cancelled paused stream emits done event', done => TESTS['a cancelled paused stream emits done event'](done)) 280 | 281 | after(done => sql.close(done)) 282 | }) 283 | 284 | describe('tvp', function () { 285 | before((done) => { 286 | sql.connect(config(), done) 287 | }) 288 | 289 | it('new Table', done => TESTS['new Table'](done)) 290 | it('Recordset.toTable()', done => TESTS['Recordset.toTable()'](done)) 291 | it('Recordset.toTable() from existing', done => TESTS['Recordset.toTable() from existing'](done)) 292 | 293 | class MSSQLTestType extends sql.Table { 294 | constructor () { 295 | super('dbo.MSSQLTestType') 296 | 297 | this.columns.add('a', sql.VarChar(50)) 298 | this.columns.add('b', sql.Int) 299 | } 300 | } 301 | 302 | it.skip('query (todo)', function (done) { 303 | const tvp = new MSSQLTestType() 304 | tvp.rows.add('asdf', 15) 305 | 306 | const r = new sql.Request() 307 | r.input('tvp', tvp) 308 | r.verbose = true 309 | r.query('select * from @tvp', function (err, result) { 310 | if (err) return done(err) 311 | 312 | assert.strictEqual(result.recordsets[0].length, 1) 313 | assert.strictEqual(result.recordsets[0][0].a, 'asdf') 314 | assert.strictEqual(result.recordsets[0][0].b, 15) 315 | 316 | return done() 317 | }) 318 | }) 319 | 320 | it.skip('prepared statement (todo)', function (done) { 321 | const tvp = new MSSQLTestType() 322 | tvp.rows.add('asdf', 15) 323 | 324 | const ps = new sql.PreparedStatement() 325 | ps.input('tvp', sql.TVP('MSSQLTestType')) 326 | ps.prepare('select * from @tvp', function (err) { 327 | if (err) { return done(err) } 328 | 329 | ps.execute({ tvp }, function (err, result) { 330 | if (err) { return done(err) } 331 | 332 | assert.strictEqual(result.recordsets[0].length, 1) 333 | assert.strictEqual(result.recordsets[0][0].a, 'asdf') 334 | assert.strictEqual(result.recordsets[0][0].b, 15) 335 | 336 | ps.unprepare(done) 337 | }) 338 | }) 339 | }) 340 | 341 | after(() => sql.close()) 342 | }) 343 | 344 | after(done => 345 | sql.connect(config(), function (err) { 346 | if (err) return done(err) 347 | 348 | const req = new sql.Request() 349 | req.query(require('node:fs').readFileSync(join(__dirname, '../cleanup.sql'), 'utf8'), function (err) { 350 | if (err) return done(err) 351 | 352 | sql.close(done) 353 | }) 354 | }) 355 | ) 356 | }) 357 | --------------------------------------------------------------------------------