UNPKG

mssql2

Version:

Microsoft SQL Server client for Node.js (fork)

497 lines (380 loc) 12.8 kB
{Pool} = require 'generic-pool' tds = require 'tds' util = require 'util' FIXED = false {TYPES, declare} = require('./datatypes') ISOLATION_LEVEL = require('./isolationlevel') ### @ignore ### castParameter = (value, type) -> unless value? then return null switch type when TYPES.VarChar, TYPES.NVarChar, TYPES.Char, TYPES.NChar, TYPES.Xml, TYPES.Text, TYPES.NText if typeof value isnt 'string' and value not instanceof String value = value.toString() when TYPES.Int, TYPES.TinyInt, TYPES.BigInt, TYPES.SmallInt if typeof value isnt 'number' and value not instanceof Number value = parseInt(value) if isNaN(value) then value = null when TYPES.Float, TYPES.Real, TYPES.Decimal, TYPES.Numeric, TYPES.SmallMoney, TYPES.Money if typeof value isnt 'number' and value not instanceof Number value = parseFloat(value) if isNaN(value) then value = null when TYPES.Bit if typeof value isnt 'boolean' and value not instanceof Boolean value = Boolean(value) when TYPES.DateTime, TYPES.SmallDateTime, TYPES.DateTimeOffset, TYPES.Date if value not instanceof Date value = new Date(value) when TYPES.Binary, TYPES.VarBinary, TYPES.Image if value not instanceof Buffer value = new Buffer(value.toString()) value ### @ignore ### createParameterHeader = (param) -> header = type: param.type.declaration switch param.type when TYPES.VarChar, TYPES.NVarChar, TYPES.VarBinary header.size = "MAX" when TYPES.Char, TYPES.NChar, TYPES.Binary header.size = param.length ? param.value?.length ? 1 header ### @ignore ### createColumns = (metadata) -> out = {} for column, index in metadata out[column.name] = index: index name: column.name length: column.length type: TYPES[column.type.sqlType] out ### @ignore ### isolationLevelDeclaration = (type) -> switch type when ISOLATION_LEVEL.READ_UNCOMMITTED then return "READ UNCOMMITTED" when ISOLATION_LEVEL.READ_COMMITTED then return "READ COMMITTED" when ISOLATION_LEVEL.REPEATABLE_READ then return "REPEATABLE READ" when ISOLATION_LEVEL.SERIALIZABLE then return "SERIALIZABLE" when ISOLATION_LEVEL.SNAPSHOT then return "SNAPSHOT" else throw new TransactionError "Invalid isolation level." ### Taken from Tedious. @private ### formatHex = (number) -> hex = number.toString(16) if hex.length == 1 hex = '0' + hex hex ### Taken from Tedious. @private ### parseGuid = (buffer) -> guid = formatHex(buffer[3]) + formatHex(buffer[2]) + formatHex(buffer[1]) + formatHex(buffer[0]) + '-' + formatHex(buffer[5]) + formatHex(buffer[4]) + '-' + formatHex(buffer[7]) + formatHex(buffer[6]) + '-' + formatHex(buffer[8]) + formatHex(buffer[9]) + '-' + formatHex(buffer[10]) + formatHex(buffer[11]) + formatHex(buffer[12]) + formatHex(buffer[13]) + formatHex(buffer[14]) + formatHex(buffer[15]) guid.toUpperCase() ### @ignore ### module.exports = (Connection, Transaction, Request, ConnectionError, TransactionError, RequestError) -> class TDSConnection extends Connection pool: null connect: (config, callback) -> cfg = userName: config.user password: config.password host: config.server port: config.port database: config.database cfg_pool = name: 'mssql' max: 10 min: 0 idleTimeoutMillis: 30000 create: (callback) => c = new tds.Connection cfg c.on 'error', (err) => if err.code is 'ECONNRESET' c.hasError = true return @emit 'error', err timeouted = false tmr = setTimeout -> timeouted = true c._client._socket.destroy() callback new ConnectionError("Connection timeout.", 'ETIMEOUT'), null # there must be a second argument null , config.timeout ? 15000 c.connect (err) => clearTimeout tmr if timeouted then return if err then err = ConnectionError err if err then return callback err, null # there must be a second argument null callback null, c validate: (c) -> c? and not c.hasError destroy: (c) -> c?.end() if config.pool for key, value of config.pool cfg_pool[key] = value @pool = Pool cfg_pool, cfg #create one testing connection to check if everything is ok @pool.acquire (err, connection) => if err and err not instanceof Error then err = new Error err if err @pool.drain => #prevent the pool from creating additional connections. we're done with it @pool?.destroyAllNow() @pool = null else # and release it immediately @pool.release connection callback err close: (callback) -> unless @pool then return callback null @pool.drain => @pool?.destroyAllNow() @pool = null callback null class TDSTransaction extends Transaction begin: (callback) -> @connection.pool.acquire (err, connection) => if err then return callback err @_pooledConnection = connection @request().query "set transaction isolation level #{isolationLevelDeclaration(@isolationLevel)}", (err) => if err then return TransactionError err connection.setAutoCommit false, callback commit: (callback) -> @_pooledConnection.commit (err) => if err then err = TransactionError err @connection.pool.release @_pooledConnection @_pooledConnection = null callback err rollback: (callback) -> @_pooledConnection.rollback (err) => if err then err = TransactionError err @connection.pool.release @_pooledConnection @_pooledConnection = null callback err class TDSRequest extends Request batch: (batch, callback) -> TDSRequest::query.call @, batch, callback bulk: (table, callback) -> process.nextTick -> callback RequestError("Bulk insert is not supported in 'msnodesql' driver.", 'ENOTSUPP') query: (command, callback) -> if @verbose and not @nested then @_log "---------- sql query ----------\n query: #{command}" if command.length is 0 return process.nextTick -> if @verbose and not @nested @_log "---------- response -----------" elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" callback? null, if @multiple or @nested then [] else null recordset = null recordsets = [] started = Date.now() handleOutput = false errors = [] lastrow = null paramHeaders = {} paramValues = {} for name, param of @parameters when param.io is 1 paramHeaders[name] = createParameterHeader param paramValues[name] = castParameter(param.value, param.type) # nested = function is called by this.execute unless @nested input = ("@#{param.name} #{declare(param.type, param)}" for name, param of @parameters when param.io is 2) output = ("@#{param.name} as '#{param.name}'" for name, param of @parameters when param.io is 2) if input.length then command = "declare #{input.join ','};#{command};" if output.length command += "select #{output.join ','};" handleOutput = true @_acquire (err, connection) => unless err if @canceled if @verbose then @_log "---------- canceling ----------" @_release connection return callback? new RequestError "Canceled.", 'ECANCEL' @_cancel = => if @verbose then @_log "---------- canceling ----------" req.cancel() req = connection.createStatement command, paramHeaders req.on 'row', (tdsrow) => row = {} for col in tdsrow.metadata.columns value = tdsrow.getValue col.name if value? # convert uniqueidentifier to string if col.type.name is 'GUIDTYPE' value = parseGuid value exi = row[col.name] if exi? if exi instanceof Array exi.push col.value else row[col.name] = [exi, value] else row[col.name] = value if @verbose @_log util.inspect(row) @_log "---------- --------------------" unless row["___return___"]? # row with ___return___ col is the last row if @stream then @emit 'row', row else lastrow = row unless @stream recordset.push row req.on 'metadata', (metadata) => recordset = [] Object.defineProperty recordset, 'columns', enumerable: false value: createColumns(metadata.columns) @nested if @stream unless recordset.columns["___return___"]? # row with ___return___ col is the last row @emit 'recordset', recordset.columns else recordsets.push recordset req.on 'done', (res) => if @canceled e = new RequestError "Canceled.", 'ECANCEL' if @stream @emit 'error', e else errors.push e unless @nested # do we have output parameters to handle? if handleOutput last = recordsets.pop()?[0] for name, param of @parameters when param.io is 2 param.value = last[param.name] if @verbose @_log " output: @#{param.name}, #{param.type.declaration}, #{param.value}" if @verbose if errors.length @_log " error: #{error}" for error in errors elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" if errors.length and not @stream error = errors.pop() error.precedingErrors = errors @_release connection if @stream callback null, if @nested then lastrow else null else callback? error, if @multiple or @nested then recordsets else recordsets[0] req.on 'message', (msg) => @emit 'info', message: msg.text number: msg.number state: msg.state class: msg.severity lineNumber: msg.lineNumber serverName: msg.serverName procName: msg.procName req.on 'error', (err) => e = RequestError err, 'EREQUEST' if @stream @emit 'error', e else errors.push e req.execute paramValues else if connection then @_release connection callback? err execute: (procedure, callback) -> if @verbose then @_log "---------- sql execute --------\n proc: #{procedure}" started = Date.now() cmd = "declare #{['@___return___ int'].concat("@#{param.name} #{declare(param.type, param)}" for name, param of @parameters when param.io is 2).join ', '};" cmd += "exec @___return___ = #{procedure} " spp = [] for name, param of @parameters if @verbose @_log " #{if param.io is 1 then " input" else "output"}: @#{param.name}, #{param.type.declaration}, #{param.value}" if param.io is 2 # output parameter spp.push "@#{param.name}=@#{param.name} output" else # input parameter spp.push "@#{param.name}=@#{param.name}" cmd += "#{spp.join ', '};" cmd += "select #{['@___return___ as \'___return___\''].concat("@#{param.name} as '#{param.name}'" for name, param of @parameters when param.io is 2).join ', '};" if @verbose then @_log "---------- response -----------" @nested = true # direct call to query, in case method on main request object is overriden (e.g. co-mssql) TDSRequest::query.call @, cmd, (err, recordsets) => @nested = false if err if @verbose elapsed = Date.now() - started @_log " error: #{err}" @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" callback? err else if @stream last = recordsets else last = recordsets.pop()?[0] if last and last.___return___? returnValue = last.___return___ for name, param of @parameters when param.io is 2 param.value = last[param.name] if @verbose @_log " output: @#{param.name}, #{param.type.declaration}, #{param.value}" if @verbose elapsed = Date.now() - started @_log " return: #{returnValue}" @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" if @stream callback null, null, returnValue else recordsets.returnValue = returnValue callback? null, recordsets, returnValue ### Cancel currently executed request. ### cancel: -> if @_cancel then return @_cancel() true return { Connection: TDSConnection Transaction: TDSTransaction Request: TDSRequest fix: -> unless FIXED require './tds-fix' FIXED = true }