// MSSQL Client // ------- const map = require('lodash/map'); const isNil = require('lodash/isNil'); const Client = require('../../client'); const MSSQL_Formatter = require('./mssql-formatter'); const Transaction = require('./transaction'); const QueryCompiler = require('./query/mssql-querycompiler'); const SchemaCompiler = require('./schema/mssql-compiler'); const TableCompiler = require('./schema/mssql-tablecompiler'); const ViewCompiler = require('./schema/mssql-viewcompiler'); const ColumnCompiler = require('./schema/mssql-columncompiler'); const QueryBuilder = require('../../query/querybuilder'); const debug = require('debug')('knex:mssql'); const SQL_INT4 = { MIN: -2147483648, MAX: 2147483647 }; const SQL_BIGINT_SAFE = { MIN: -9007199254740991, MAX: 9007199254740991 }; // Always initialize with the "QueryBuilder" and "QueryCompiler" objects, which // extend the base 'lib/query/builder' and 'lib/query/compiler', respectively. class Client_MSSQL extends Client { constructor(config = {}) { super(config); } /** * @param {import('knex').Config} options */ _generateConnection() { const settings = this.connectionSettings; settings.options = settings.options || {}; /** @type {import('tedious').ConnectionConfig} */ const cfg = { authentication: { type: settings.type || 'default', options: { userName: settings.userName || settings.user, password: settings.password, domain: settings.domain, token: settings.token, clientId: settings.clientId, clientSecret: settings.clientSecret, tenantId: settings.tenantId, msiEndpoint: settings.msiEndpoint, }, }, server: settings.server || settings.host, options: { database: settings.database, encrypt: settings.encrypt || false, port: settings.port || 1433, connectTimeout: settings.connectionTimeout || settings.timeout || 15000, requestTimeout: !isNil(settings.requestTimeout) ? settings.requestTimeout : 15000, rowCollectionOnDone: false, rowCollectionOnRequestCompletion: false, useColumnNames: false, tdsVersion: settings.options.tdsVersion || '7_4', appName: settings.options.appName || 'knex', trustServerCertificate: false, ...settings.options, }, }; // tedious always connect via tcp when port is specified if (cfg.options.instanceName) delete cfg.options.port; if (isNaN(cfg.options.requestTimeout)) cfg.options.requestTimeout = 15000; if (cfg.options.requestTimeout === Infinity) cfg.options.requestTimeout = 0; if (cfg.options.requestTimeout < 0) cfg.options.requestTimeout = 0; if (settings.debug) { cfg.options.debug = { packet: true, token: true, data: true, payload: true, }; } return cfg; } _driver() { const tds = require('tedious'); return tds; } formatter() { return new MSSQL_Formatter(this, ...arguments); } transaction() { return new Transaction(this, ...arguments); } queryCompiler() { return new QueryCompiler(this, ...arguments); } schemaCompiler() { return new SchemaCompiler(this, ...arguments); } tableCompiler() { return new TableCompiler(this, ...arguments); } viewCompiler() { return new ViewCompiler(this, ...arguments); } queryBuilder() { const b = new QueryBuilder(this); return b; } columnCompiler() { return new ColumnCompiler(this, ...arguments); } wrapIdentifierImpl(value) { if (value === '*') { return '*'; } return `[${value.replace(/[[\]]+/g, '')}]`; } // Get a raw connection, called by the `pool` whenever a new // connection needs to be added to the pool. acquireRawConnection() { return new Promise((resolver, rejecter) => { debug('connection::connection new connection requested'); const Driver = this._driver(); const settings = Object.assign({}, this._generateConnection()); const connection = new Driver.Connection(settings); connection.connect((err) => { if (err) { debug('connection::connect error: %s', err.message); return rejecter(err); } debug('connection::connect connected to server'); connection.connected = true; connection.on('error', (e) => { debug('connection::error message=%s', e.message); connection.__knex__disposed = e; connection.connected = false; }); connection.once('end', () => { connection.connected = false; connection.__knex__disposed = 'Connection to server was terminated.'; debug('connection::end connection ended.'); }); return resolver(connection); }); }); } validateConnection(connection) { return connection && connection.connected; } // Used to explicitly close a connection, called internally by the pool // when a connection times out or the pool is shutdown. destroyRawConnection(connection) { debug('connection::destroy'); return new Promise((resolve) => { connection.once('end', () => { resolve(); }); connection.close(); }); } // Position the bindings for the query. positionBindings(sql) { let questionCount = -1; return sql.replace(/\\?\?/g, (match) => { if (match === '\\?') { return '?'; } questionCount += 1; return `@p${questionCount}`; }); } _chomp(connection) { if (connection.state.name === 'LoggedIn') { const nextRequest = this.requestQueue.pop(); if (nextRequest) { debug( 'connection::query executing query, %d more in queue', this.requestQueue.length ); connection.execSql(nextRequest); } } } _enqueueRequest(request, connection) { this.requestQueue.push(request); this._chomp(connection); } _makeRequest(query, callback) { const Driver = this._driver(); const sql = typeof query === 'string' ? query : query.sql; let rowCount = 0; if (!sql) throw new Error('The query is empty'); debug('request::request sql=%s', sql); const request = new Driver.Request(sql, (err, remoteRowCount) => { if (err) { debug('request::error message=%s', err.message); return callback(err); } rowCount = remoteRowCount; debug('request::callback rowCount=%d', rowCount); }); request.on('prepared', () => { debug('request %s::request prepared', this.id); }); request.on('done', (rowCount, more) => { debug('request::done rowCount=%d more=%s', rowCount, more); }); request.on('doneProc', (rowCount, more) => { debug( 'request::doneProc id=%s rowCount=%d more=%s', request.id, rowCount, more ); }); request.on('doneInProc', (rowCount, more) => { debug( 'request::doneInProc id=%s rowCount=%d more=%s', request.id, rowCount, more ); }); request.once('requestCompleted', () => { debug('request::completed id=%s', request.id); return callback(null, rowCount); }); request.on('error', (err) => { debug('request::error id=%s message=%s', request.id, err.message); return callback(err); }); return request; } // Grab a connection, run the query via the MSSQL streaming interface, // and pass that through to the stream we've sent back to the client. _stream(connection, query, /** @type {NodeJS.ReadWriteStream} */ stream) { return new Promise((resolve, reject) => { const request = this._makeRequest(query, (err) => { if (err) { stream.emit('error', err); return reject(err); } resolve(); }); request.on('row', (row) => { stream.write( row.reduce( (prev, curr) => ({ ...prev, [curr.metadata.colName]: curr.value, }), {} ) ); }); request.on('error', (err) => { stream.emit('error', err); reject(err); }); request.once('requestCompleted', () => { stream.end(); resolve(); }); this._assignBindings(request, query.bindings); this._enqueueRequest(request, connection); }); } _assignBindings(request, bindings) { if (Array.isArray(bindings)) { for (let i = 0; i < bindings.length; i++) { const binding = bindings[i]; this._setReqInput(request, i, binding); } } } _scaleForBinding(binding) { if (binding % 1 === 0) { throw new Error(`The binding value ${binding} must be a decimal number.`); } return { scale: 10 }; } _typeForBinding(binding) { const Driver = this._driver(); if ( this.connectionSettings.options && this.connectionSettings.options.mapBinding ) { const result = this.connectionSettings.options.mapBinding(binding); if (result) { return [result.value, result.type]; } } switch (typeof binding) { case 'string': return [binding, Driver.TYPES.NVarChar]; case 'boolean': return [binding, Driver.TYPES.Bit]; case 'number': { if (binding % 1 !== 0) { return [binding, Driver.TYPES.Float]; } if (binding < SQL_INT4.MIN || binding > SQL_INT4.MAX) { if (binding < SQL_BIGINT_SAFE.MIN || binding > SQL_BIGINT_SAFE.MAX) { throw new Error( `Bigint must be safe integer or must be passed as string, saw ${binding}` ); } return [binding, Driver.TYPES.BigInt]; } return [binding, Driver.TYPES.Int]; } default: { if (binding instanceof Date) { return [binding, Driver.TYPES.DateTime]; } if (binding instanceof Buffer) { return [binding, Driver.TYPES.VarBinary]; } return [binding, Driver.TYPES.NVarChar]; } } } // Runs the query on the specified connection, providing the bindings // and any other necessary prep work. _query(connection, query) { return new Promise((resolve, reject) => { const rows = []; const request = this._makeRequest(query, (err, count) => { if (err) { return reject(err); } query.response = rows; process.nextTick(() => this._chomp(connection)); resolve(query); }); request.on('row', (row) => { debug('request::row'); rows.push(row); }); this._assignBindings(request, query.bindings); this._enqueueRequest(request, connection); }); } // sets a request input parameter. Detects bigints and decimals and sets type appropriately. _setReqInput(req, i, inputBinding) { const [binding, tediousType] = this._typeForBinding(inputBinding); const bindingName = 'p'.concat(i); let options; if (typeof binding === 'number' && binding % 1 !== 0) { options = this._scaleForBinding(binding); } debug( 'request::binding pos=%d type=%s value=%s', i, tediousType.name, binding ); if (Buffer.isBuffer(binding)) { options = { length: 'max', }; } req.addParameter(bindingName, tediousType, binding, options); } // Process the response as returned from the query. processResponse(query, runner) { if (query == null) return; let { response } = query; const { method } = query; if (query.output) { return query.output.call(runner, response); } response = response.map((row) => row.reduce((columns, r) => { const colName = r.metadata.colName; if (columns[colName]) { if (!Array.isArray(columns[colName])) { columns[colName] = [columns[colName]]; } columns[colName].push(r.value); } else { columns[colName] = r.value; } return columns; }, {}) ); if (query.output) return query.output.call(runner, response); switch (method) { case 'select': return response; case 'first': return response[0]; case 'pluck': return map(response, query.pluck); case 'insert': case 'del': case 'update': case 'counter': if (query.returning) { if (query.returning === '@@rowcount') { return response[0]['']; } } return response; default: return response; } } } Object.assign(Client_MSSQL.prototype, { requestQueue: [], dialect: 'mssql', driverName: 'mssql', }); module.exports = Client_MSSQL;