// MSSQL Client // ------- const { map, flatten, values } = require('lodash'); const inherits = require('inherits'); const Client = require('../../client'); const Bluebird = require('bluebird'); const Formatter = require('../../formatter'); const Transaction = require('./transaction'); const QueryCompiler = require('./query/compiler'); const SchemaCompiler = require('./schema/compiler'); const TableCompiler = require('./schema/tablecompiler'); const ColumnCompiler = require('./schema/columncompiler'); 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. function Client_MSSQL(config = {}) { // #1235 mssql module wants 'server', not 'host'. This is to enforce the same // options object across all dialects. if (config && config.connection && config.connection.host) { config.connection.server = config.connection.host; } // mssql always creates pool :( lets try to unpool it as much as possible this.mssqlPoolSettings = { min: 1, max: 1, idleTimeoutMillis: Number.MAX_SAFE_INTEGER, evictionRunIntervalMillis: 0, }; Client.call(this, config); } inherits(Client_MSSQL, Client); Object.assign(Client_MSSQL.prototype, { dialect: 'mssql', driverName: 'mssql', _driver() { const tds = require('tedious'); const mssqlTedious = require('mssql'); const base = require('mssql/lib/base'); // Monkey patch mssql's tedious driver _poolCreate method to fix problem with hanging acquire // connection, this should be removed when https://github.com/tediousjs/node-mssql/pull/614 is // merged and released. // Also since this dialect actually always uses tedious driver (msnodesqlv8 driver should be // required in different way), it might be better to use tedious directly, because mssql // driver uses always internally extra generic-pool and just adds one unnecessary layer of // indirection between database and knex and mssql driver has been lately without maintainer // (changing implementation to use tedious will be breaking change though). // TODO: remove mssql implementation all together and use tedious directly /* istanbul ignore next */ const mssqlVersion = require('mssql/package.json').version; /* istanbul ignore next */ if (mssqlVersion === '4.1.0') { mssqlTedious.ConnectionPool.prototype.release = release; mssqlTedious.ConnectionPool.prototype._poolCreate = _poolCreate; } else { const [major] = mssqlVersion.split('.'); // if version is not ^5.0.0 if (major < 5) { throw new Error( 'This knex version only supports mssql driver versions 4.1.0 and 5.0.0+' ); } } /* istanbul ignore next */ // in some rare situations release is called when stream is interrupted, but // after pool is already destroyed function release(connection) { if (this.pool) { this.pool.release(connection); } } /* istanbul ignore next */ function _poolCreate() { // implementation is copy-pasted from https://github.com/tediousjs/node-mssql/pull/614 return new base.Promise((resolve, reject) => { const cfg = { userName: this.config.user, password: this.config.password, server: this.config.server, options: Object.assign({}, this.config.options), domain: this.config.domain, }; cfg.options.database = this.config.database; cfg.options.port = this.config.port; cfg.options.connectTimeout = this.config.connectionTimeout || this.config.timeout || 15000; cfg.options.requestTimeout = this.config.requestTimeout != null ? this.config.requestTimeout : 15000; cfg.options.tdsVersion = cfg.options.tdsVersion || '7_4'; cfg.options.rowCollectionOnDone = false; cfg.options.rowCollectionOnRequestCompletion = false; cfg.options.useColumnNames = false; cfg.options.appName = cfg.options.appName || 'node-mssql'; // 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 (this.config.debug) { cfg.options.debug = { packet: true, token: true, data: true, payload: true, }; } const tedious = new tds.Connection(cfg); // prevent calling resolve again on end event let alreadyResolved = false; function safeResolve(err) { if (!alreadyResolved) { alreadyResolved = true; resolve(err); } } function safeReject(err) { if (!alreadyResolved) { alreadyResolved = true; reject(err); } } tedious.once('end', (evt) => { safeReject( new base.ConnectionError( 'Connection ended unexpectedly during connecting' ) ); }); tedious.once('connect', (err) => { if (err) { err = new base.ConnectionError(err); return safeReject(err); } safeResolve(tedious); }); tedious.on('error', (err) => { if (err.code === 'ESOCKET') { tedious.hasError = true; return; } this.emit('error', err); }); if (this.config.debug) { tedious.on('debug', this.emit.bind(this, 'debug', tedious)); } }); } return mssqlTedious; }, 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); }, 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 Bluebird((resolver, rejecter) => { const settings = Object.assign({}, this.connectionSettings); settings.pool = this.mssqlPoolSettings; const connection = new this.driver.ConnectionPool(settings); connection.connect((err) => { if (err) { return rejecter(err); } connection.on('error', (err) => { connection.__knex__disposed = err; }); resolver(connection); }); }); }, validateConnection(connection) { if (connection.connected === true) { return true; } return false; }, // Used to explicitly close a connection, called internally by the pool // when a connection times out or the pool is shutdown. destroyRawConnection(connection) { return connection.close().catch((err) => { // some times close will reject just because pool has already been destoyed // internally by the driver there is nothing we can do in this case }); }, // Position the bindings for the query. positionBindings(sql) { let questionCount = -1; return sql.replace(/\?/g, function() { questionCount += 1; return `@p${questionCount}`; }); }, // 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, obj, stream) { if (!obj || typeof obj === 'string') obj = { sql: obj }; return new Bluebird((resolver, rejecter) => { stream.on('error', (err) => { rejecter(err); }); stream.on('end', resolver); const { sql } = obj; if (!sql) return resolver(); const req = (connection.tx_ || connection).request(); //req.verbose = true; req.multiple = true; req.stream = true; if (obj.bindings) { for (let i = 0; i < obj.bindings.length; i++) { this._setReqInput(req, i, obj.bindings[i]); } } req.pipe(stream); req.query(sql); }); }, // Runs the query on the specified connection, providing the bindings // and any other necessary prep work. _query(connection, obj) { const client = this; if (!obj || typeof obj === 'string') obj = { sql: obj }; return new Bluebird((resolver, rejecter) => { const { sql } = obj; if (!sql) return resolver(); const req = (connection.tx_ || connection).request(); // req.verbose = true; req.multiple = true; if (obj.bindings) { for (let i = 0; i < obj.bindings.length; i++) { client._setReqInput(req, i, obj.bindings[i]); } } req.query(sql, (err, recordset) => { if (err) { return rejecter(err); } obj.response = recordset.recordsets[0]; resolver(obj); }); }); }, // sets a request input parameter. Detects bigints and decimals and sets type appropriately. _setReqInput(req, i, binding) { if (typeof binding == 'number') { if (binding % 1 !== 0) { req.input(`p${i}`, this.driver.Decimal(38, 10), binding); } else 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}` ); } req.input(`p${i}`, this.driver.BigInt, binding); } else { req.input(`p${i}`, this.driver.Int, binding); } } else { req.input(`p${i}`, binding); } }, // Process the response as returned from the query. processResponse(obj, runner) { if (obj == null) return; const { response, method } = obj; if (obj.output) return obj.output.call(runner, response); switch (method) { case 'select': case 'pluck': case 'first': if (method === 'pluck') return map(response, obj.pluck); return method === 'first' ? response[0] : response; case 'insert': case 'del': case 'update': case 'counter': if (obj.returning) { if (obj.returning === '@@rowcount') { return response[0]['']; } if ( (Array.isArray(obj.returning) && obj.returning.length > 1) || obj.returning[0] === '*' ) { return response; } // return an array with values if only one returning value was specified return flatten(map(response, values)); } return response; default: return response; } }, }); class MSSQL_Formatter extends Formatter { // Accepts a string or array of columns to wrap as appropriate. columnizeWithPrefix(prefix, target) { const columns = typeof target === 'string' ? [target] : target; let str = '', i = -1; while (++i < columns.length) { if (i > 0) str += ', '; str += prefix + this.wrap(columns[i]); } return str; } } module.exports = Client_MSSQL;