UNPKG

mssql2

Version:

Microsoft SQL Server client for Node.js (fork)

1,006 lines (783 loc) 29.8 kB
{Pool} = require 'generic-pool' tds = require 'tedious' util = require 'util' {TYPES, declare, cast} = require './datatypes' DECLARATIONS = require('./datatypes').DECLARATIONS UDT = require('./udt').PARSERS Table = require('./table') JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B' XML_COLUMN_ID = 'XML_F52E2B61-18A1-11d1-B105-00805F49916B' ### @ignore ### bindDomain = (cb) -> if process.domain process.domain?.bind cb else cb ### @ignore ### getTediousType = (type) -> switch type when TYPES.VarChar then return tds.TYPES.VarChar when TYPES.NVarChar then return tds.TYPES.NVarChar when TYPES.Text then return tds.TYPES.Text when TYPES.Int then return tds.TYPES.Int when TYPES.BigInt then return tds.TYPES.BigInt when TYPES.TinyInt then return tds.TYPES.TinyInt when TYPES.SmallInt then return tds.TYPES.SmallInt when TYPES.Bit then return tds.TYPES.Bit when TYPES.Float then return tds.TYPES.Float when TYPES.Decimal then return tds.TYPES.Decimal when TYPES.Numeric then return tds.TYPES.Numeric when TYPES.Real then return tds.TYPES.Real when TYPES.Money then return tds.TYPES.Money when TYPES.SmallMoney then return tds.TYPES.SmallMoney when TYPES.Time then return tds.TYPES.TimeN when TYPES.Date then return tds.TYPES.DateN when TYPES.DateTime then return tds.TYPES.DateTime when TYPES.DateTime2 then return tds.TYPES.DateTime2N when TYPES.DateTimeOffset then return tds.TYPES.DateTimeOffsetN when TYPES.SmallDateTime then return tds.TYPES.SmallDateTime when TYPES.UniqueIdentifier then return tds.TYPES.UniqueIdentifierN when TYPES.Xml then return tds.TYPES.VarChar when TYPES.Char then return tds.TYPES.Char when TYPES.NChar then return tds.TYPES.NChar when TYPES.NText then return tds.TYPES.NVarChar when TYPES.Image then return tds.TYPES.Image when TYPES.Binary then return tds.TYPES.Binary when TYPES.VarBinary then return tds.TYPES.VarBinary when TYPES.UDT, TYPES.Geography, TYPES.Geometry then return tds.TYPES.UDT when TYPES.TVP then return tds.TYPES.TVP when TYPES.Variant then return tds.TYPES.Variant else return type ### @ignore ### getMssqlType = (type, length) -> switch type when tds.TYPES.Char then return TYPES.Char when tds.TYPES.NChar then return TYPES.NChar when tds.TYPES.VarChar then return TYPES.VarChar when tds.TYPES.NVarChar then return TYPES.NVarChar when tds.TYPES.Text then return TYPES.Text when tds.TYPES.NText then return TYPES.NText when tds.TYPES.Int then return TYPES.Int when tds.TYPES.IntN if length is 8 then return TYPES.BigInt if length is 4 then return TYPES.Int if length is 2 then return TYPES.SmallInt return TYPES.TinyInt when tds.TYPES.BigInt then return TYPES.BigInt when tds.TYPES.TinyInt then return TYPES.TinyInt when tds.TYPES.SmallInt then return TYPES.SmallInt when tds.TYPES.Bit, tds.TYPES.BitN then return TYPES.Bit when tds.TYPES.Float then return TYPES.Float when tds.TYPES.FloatN if length is 8 then return TYPES.FloatN return TYPES.Real when tds.TYPES.Real then return TYPES.Real when tds.TYPES.Money then return TYPES.Money when tds.TYPES.MoneyN if length is 8 then return TYPES.Money return TYPES.SmallMoney when tds.TYPES.SmallMoney then return TYPES.SmallMoney when tds.TYPES.Numeric, tds.TYPES.NumericN then return TYPES.Numeric when tds.TYPES.Decimal, tds.TYPES.DecimalN then return TYPES.Decimal when tds.TYPES.DateTime then return TYPES.DateTime when tds.TYPES.DateTimeN if length is 8 then return TYPES.DateTime return TYPES.SmallDateTime when tds.TYPES.TimeN then return TYPES.Time when tds.TYPES.DateN then return TYPES.Date when tds.TYPES.DateTime2N then return TYPES.DateTime2 when tds.TYPES.DateTimeOffsetN then return TYPES.DateTimeOffset when tds.TYPES.SmallDateTime then return TYPES.SmallDateTime when tds.TYPES.UniqueIdentifierN then return TYPES.UniqueIdentifier when tds.TYPES.Image then return TYPES.Image when tds.TYPES.Binary then return TYPES.Binary when tds.TYPES.VarBinary then return TYPES.VarBinary when tds.TYPES.Xml then return TYPES.Xml when tds.TYPES.UDT then return TYPES.UDT when tds.TYPES.TVP then return TYPES.TVP when tds.TYPES.Variant then return TYPES.Variant ### @ignore ### createColumns = (metadata) -> out = {} for column, index in metadata out[column.colName] = index: index name: column.colName length: column.dataLength type: getMssqlType(column.type, column.dataLength) scale: column.scale precision: column.precision nullable: !!(column.flags & 0x01) caseSensitive: !!(column.flags & 0x02) identity: !!(column.flags & 0x10) readOnly: !(column.flags & 0x0C) if column.udtInfo? out[column.colName].udt = name: column.udtInfo.typeName database: column.udtInfo.dbname schema: column.udtInfo.owningSchema assembly: column.udtInfo.assemblyName if DECLARATIONS[column.udtInfo.typeName] out[column.colName].type = DECLARATIONS[column.udtInfo.typeName] out ### @ignore ### valueCorrection = (value, metadata) -> if metadata.type is tds.TYPES.UDT and value? if UDT[metadata.udtInfo.typeName] UDT[metadata.udtInfo.typeName] value else value else value ### @ignore ### parameterCorrection = (value) -> if value instanceof Table tvp = name: value.name schema: value.schema columns: [] rows: value.rows for col in value.columns tvp.columns.push name: col.name type: getTediousType col.type length: col.length scale: col.scale precision: col.precision tvp else value ### @ignore ### module.exports = (Connection, Transaction, Request, ConnectionError, TransactionError, RequestError) -> class TediousConnection extends Connection pool: null connect: (config, callback) -> cfg = userName: config.user password: config.password server: config.server options: config.options domain: config.domain cfg.options.database ?= config.database cfg.options.port ?= config.port cfg.options.connectTimeout ?= config.connectionTimeout ? config.timeout ? 15000 # config.timeout deprecated in 0.6.0 cfg.options.requestTimeout ?= config.requestTimeout ? 15000 cfg.options.tdsVersion ?= '7_4' cfg.options.rowCollectionOnDone = false cfg.options.rowCollectionOnRequestCompletion = false cfg.options.useColumnNames = false cfg.options.appName ?= 'node-mssql' # tedious always connect via tcp when port is specified if cfg.options.instanceName then delete cfg.options.port if isNaN cfg.options.requestTimeout then cfg.options.requestTimeout = 15000 if cfg.options.requestTimeout is Infinity then cfg.options.requestTimeout = 0 if cfg.options.requestTimeout < 0 then cfg.options.requestTimeout = 0 if config.debug cfg.options.debug = packet: true token: true data: true payload: true cfg_pool = name: 'mssql' max: 10 min: 0 idleTimeoutMillis: 30000 create: (callback) => c = new tds.Connection cfg c.once 'connect', (err) -> if err then err = ConnectionError err if err then return callback err, null # there must be a second argument null callback null, c c.on 'error', (err) => if err.code is 'ESOCKET' c.hasError = true return @emit 'error', err if config.debug c.on 'debug', (msg) => @_debug msg validate: (c) -> c? and not c.closed and not c.hasError destroy: (c) -> c?.close() # there might be some unemitted events setTimeout -> c?.removeAllListeners() , 500 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 @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 TediousTransaction extends Transaction _abort: -> if not @_rollbackRequested # transaction interrupted because of XACT_ABORT pc = @_pooledConnection setImmediate => # defer releasing so connection can switch from SentClientRequest to LoggedIn state @connection.pool.release pc @_pooledConnection.removeListener 'rollbackTransaction', @_abort @_pooledConnection = null @_aborted = true @emit 'rollback', true begin: (callback) -> @_aborted = false @_rollbackRequested = false @connection.pool.acquire (err, connection) => if err then return callback err @_pooledConnection = connection @_pooledConnection.on 'rollbackTransaction', @_abort connection.beginTransaction bindDomain (err) => if err then err = TransactionError err callback err , @name, @isolationLevel commit: (callback) -> @_pooledConnection.commitTransaction bindDomain (err) => if err then err = TransactionError err @_pooledConnection.removeListener 'rollbackTransaction', @_abort @connection.pool.release @_pooledConnection @_pooledConnection = null callback err rollback: (callback) -> @_rollbackRequested = true @_pooledConnection.rollbackTransaction bindDomain (err) => if err then err = TransactionError err @_pooledConnection.removeListener 'rollbackTransaction', @_abort @connection.pool.release @_pooledConnection @_pooledConnection = null callback err class TediousRequest extends Request ### Execute specified sql batch. ### batch: (batch, callback) -> @_isBatch = true TediousRequest::query.call @, batch, callback ### Bulk load. ### bulk: (table, callback) -> table._makeBulk() unless table.name process.nextTick -> callback RequestError("Table name must be specified for bulk insert.", "ENAME") if table.name.charAt(0) is '@' process.nextTick -> callback RequestError("You can't use table variables for bulk insert.", "ENAME") started = Date.now() errors = [] errorHandlers = {} hasReturned = false handleError = (doReturn, connection, info) => err = new Error info.message err.info = info e = RequestError err, 'EREQUEST' if @stream @emit 'error', e else if doReturn and not hasReturned if connection? for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true callback?(e) # we must collect errors even in stream mode errors.push e handleInfo = (msg) => @emit 'info', message: msg.message number: msg.number state: msg.state class: msg.class lineNumber: msg.lineNumber serverName: msg.serverName procName: msg.procName @_acquire (err, connection) => unless err if @verbose then @_log "-------- sql bulk load --------\n table: #{table.name}" if @canceled if @verbose then @_log "---------- canceling ----------" @_release connection return callback? new RequestError "Canceled.", 'ECANCEL' @_cancel = => if @verbose then @_log "---------- canceling ----------" connection.cancel() # attach handler to handle multiple error messages errorHandlers['infoMessage'] = handleInfo errorHandlers['errorMessage'] = handleError.bind(undefined, false, connection) errorHandlers['error'] = handleError.bind(undefined, true, connection) connection.on 'infoMessage', errorHandlers['infoMessage'] connection.on 'errorMessage', errorHandlers['errorMessage'] connection.on 'error', errorHandlers['error'] done = bindDomain (err, rowCount) => # to make sure we handle no-sql errors as well if err and err.message isnt errors[errors.length - 1]?.message err = RequestError err, 'EREQUEST' if @stream @emit 'error', err errors.push err # TODO ---- if @verbose if errors.length @_log " error: #{error}" for error in errors elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_cancel = null if errors.length and not @stream error = errors.pop() error.precedingErrors = errors if not hasReturned for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true if @stream callback null, null else callback? error, rowCount bulk = connection.newBulkLoad table.path, done for col in table.columns bulk.addColumn col.name, getTediousType(col.type), {nullable: col.nullable, length: col.length, scale: col.scale, precision: col.precision} for row in table.rows bulk.addRow row if @verbose then @_log "---------- response -----------" if table.create if table.temporary objectid = "tempdb..[#{table.name}]" else objectid = table.path req = new tds.Request "if object_id('#{objectid.replace(/'/g, '\'\'')}') is null #{table.declare()}", (err) => if err then return done err connection.execBulkLoad bulk connection.execSqlBatch req else connection.execBulkLoad bulk ### Execute specified sql command. ### query: (command, callback) -> columns = {} recordset = [] recordsets = [] started = Date.now() errors = [] batchLastRow = null batchHasOutput = false isJSONRecordset = false isChunkedRecordset = false chunksBuffer = null xmlBuffer = null hasReturned = false errorHandlers = {} handleError = (doReturn, connection, info) => err = new Error info.message err.info = info e = RequestError err, 'EREQUEST' if @stream @emit 'error', e else if doReturn and not hasReturned if connection? for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true callback? e # we must collect errors even in stream mode errors.push e handleInfo = (msg) => @emit 'info', message: msg.message number: msg.number state: msg.state class: msg.class lineNumber: msg.lineNumber serverName: msg.serverName procName: msg.procName @_acquire (err, connection) => unless err if @verbose then @_log "---------- sql #{if @_isBatch then 'batch' else 'query'} ----------\n #{if @_isBatch then 'batch' else 'query'}: #{command}" if @canceled if @verbose then @_log "---------- canceling ----------" @_release connection return callback? new RequestError "Canceled.", 'ECANCEL' @_cancel = => if @verbose then @_log "---------- canceling ----------" connection.cancel() # attach handler to handle multiple error messages errorHandlers['infoMessage'] = handleInfo errorHandlers['errorMessage'] = handleError.bind(undefined, false, connection) errorHandlers['error'] = handleError.bind(undefined, true, connection) connection.on 'infoMessage', errorHandlers['infoMessage'] connection.on 'errorMessage', errorHandlers['errorMessage'] connection.on 'error', errorHandlers['error'] req = new tds.Request command, bindDomain (err) => # to make sure we handle no-sql errors as well if err and err.message isnt errors[errors.length - 1]?.message err = RequestError err, 'EREQUEST' if @stream @emit 'error', err errors.push err # process batch outputs if batchHasOutput unless @stream batchLastRow = recordsets.pop()[0] for name, value of batchLastRow when name isnt '___return___' if @verbose if value is tds.TYPES.Null @_log " output: @#{name}, null" else @_log " output: @#{name}, #{@parameters[name].type.declaration.toLowerCase()}, #{value}" @parameters[name].value = if value is tds.TYPES.Null then null else value if @verbose if errors.length @_log " error: #{error}" for error in errors elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_cancel = null if errors.length and not @stream error = errors.pop() error.precedingErrors = errors if not hasReturned for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true if @stream callback null, null else callback? error, if @multiple then recordsets else recordsets[0] req.on 'columnMetadata', (metadata) => columns = createColumns metadata isChunkedRecordset = false if metadata.length is 1 and metadata[0].colName in [JSON_COLUMN_ID, XML_COLUMN_ID] isChunkedRecordset = true chunksBuffer = [] if @stream if @_isBatch # don't stream recordset with output values in batches unless columns["___return___"]? @emit 'recordset', columns else @emit 'recordset', columns doneHandler = (rowCount, more) => # this function is called even when select only set variables so we should skip adding a new recordset if Object.keys(columns).length is 0 @rowsAffected += rowCount if rowCount > 0 return if isChunkedRecordset if columns[JSON_COLUMN_ID] and @connection.config.parseJSON is true try row = JSON.parse chunksBuffer.join '' catch ex row = null ex = RequestError new Error("Failed to parse incoming JSON. #{ex.message}"), 'EJSON' if @stream @emit 'error', ex # we must collect errors even in stream mode errors.push ex else row = {} row[Object.keys(columns)[0]] = chunksBuffer.join '' chunksBuffer = null if @verbose @_log util.inspect row @_log "---------- --------------------" if @stream @emit 'row', row else recordset.push row unless @stream # all rows of current recordset loaded Object.defineProperty recordset, 'columns', enumerable: false value: columns Object.defineProperty recordset, 'toTable', enumerable: false value: -> Table.fromRecordset @ recordsets.push recordset recordset = [] columns = {} req.on 'doneInProc', doneHandler # doneInProc handlers are used in both queries and batches req.on 'done', doneHandler # done handlers are used in batches req.on 'returnValue', (parameterName, value, metadata) => if @verbose if value is tds.TYPES.Null @_log " output: @#{parameterName}, null" else @_log " output: @#{parameterName}, #{@parameters[parameterName].type.declaration.toLowerCase()}, #{value}" @parameters[parameterName].value = if value is tds.TYPES.Null then null else value req.on 'row', (columns) => unless recordset recordset = [] if isChunkedRecordset chunksBuffer.push columns[0].value else row = {} for col in columns col.value = valueCorrection col.value, col.metadata exi = row[col.metadata.colName] if exi? if exi instanceof Array exi.push col.value else row[col.metadata.colName] = [exi, col.value] else row[col.metadata.colName] = col.value if @verbose @_log util.inspect(row) @_log "---------- --------------------" if @stream if @_isBatch # dont stream recordset with output values in batches if row["___return___"]? batchLastRow = row else @emit 'row', row else @emit 'row', row else recordset.push row if @_isBatch if Object.keys(@parameters).length for name, param of @parameters value = getTediousType(param.type).validate param.value if value instanceof TypeError value = new RequestError "Validation failed for parameter \'#{name}\'. #{value.message}", 'EPARAM' if @verbose @_log " error: #{value}" @_log "---------- completed ----------" @_release connection return callback? value param.value = value declarations = ("@#{name} #{declare(param.type, param)}" for name, param of @parameters) assigns = ("@#{name} = #{cast(param.value, param.type, param)}" for name, param of @parameters) selects = ("@#{name} as [#{name}]" for name, param of @parameters when param.io is 2) batchHasOutput = selects.length > 0 req.sqlTextOrProcedure = "declare #{declarations.join(', ')};select #{assigns.join(', ')};#{req.sqlTextOrProcedure};#{if batchHasOutput then ('select 1 as [___return___], '+ selects.join(', ')) else ''}" else for name, param of @parameters if @verbose if param.value is tds.TYPES.Null @_log " #{if param.io is 1 then " input" else "output"}: @#{param.name}, null" else @_log " #{if param.io is 1 then " input" else "output"}: @#{param.name}, #{param.type.declaration.toLowerCase()}, #{param.value}" if param.io is 1 req.addParameter param.name, getTediousType(param.type), parameterCorrection(param.value), {length: param.length, scale: param.scale, precision: param.precision} else req.addOutputParameter param.name, getTediousType(param.type), parameterCorrection(param.value), {length: param.length, scale: param.scale, precision: param.precision} if @verbose then @_log "---------- response -----------" connection[if @_isBatch then 'execSqlBatch' else 'execSql'] req else if connection then @_release connection callback? err ### Execute stored procedure with specified parameters. ### execute: (procedure, callback) -> columns = {} recordset = [] recordsets = [] returnValue = 0 started = Date.now() errors = [] isChunkedRecordset = false chunksBuffer = null hasReturned = false errorHandlers = {} handleError = (doReturn, connection, info) => err = new Error info.message err.info = info e = RequestError err, 'EREQUEST' if @stream @emit 'error', e else if doReturn and not hasReturned if connection? for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true callback? e # we must collect errors even in stream mode errors.push e @_acquire (err, connection) => unless err if @verbose then @_log "---------- sql execute --------\n proc: #{procedure}" if @canceled if @verbose then @_log "---------- canceling ----------" @_release connection return callback? new RequestError "Canceled.", 'ECANCEL' @_cancel = => if @verbose then @_log "---------- canceling ----------" connection.cancel() # attach handler to handle multiple error messages errorHandlers['errorMessage'] = handleError.bind(undefined, false, connection) errorHandlers['error'] = handleError.bind(undefined, true, connection) connection.on 'errorMessage', errorHandlers['errorMessage'] connection.on 'error', errorHandlers['error'] req = new tds.Request procedure, bindDomain (err) => # to make sure we handle no-sql errors as well if err and err.message isnt errors[errors.length - 1]?.message err = RequestError err, 'EREQUEST' if @stream @emit 'error', err errors.push err if @verbose if errors.length @_log " error: #{error}" for error in errors elapsed = Date.now() - started @_log " return: #{returnValue}" @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_cancel = null if errors.length and not @stream error = errors.pop() error.precedingErrors = errors if not hasReturned for event, handler of errorHandlers connection.removeListener event, handler @_release connection hasReturned = true if @stream callback null, null, returnValue else recordsets.returnValue = returnValue callback? error, recordsets, returnValue req.on 'columnMetadata', (metadata) => columns = createColumns metadata isChunkedRecordset = false if metadata.length is 1 and metadata[0].colName in [JSON_COLUMN_ID, XML_COLUMN_ID] isChunkedRecordset = true chunksBuffer = [] if @stream @emit 'recordset', columns req.on 'row', (columns) => unless recordset recordset = [] if isChunkedRecordset chunksBuffer.push columns[0].value else row = {} for col in columns col.value = valueCorrection col.value, col.metadata exi = row[col.metadata.colName] if exi? if exi instanceof Array exi.push col.value else row[col.metadata.colName] = [exi, col.value] else row[col.metadata.colName] = col.value if @verbose @_log util.inspect(row) @_log "---------- --------------------" if @stream @emit 'row', row else recordset.push row req.on 'doneInProc', (rowCount, more) => # filter empty recordsets when NOCOUNT is OFF if Object.keys(columns).length is 0 @rowsAffected += rowCount if rowCount > 0 return if isChunkedRecordset if columns[JSON_COLUMN_ID] and @connection.config.parseJSON is true try row = JSON.parse chunksBuffer.join '' catch ex row = null ex = RequestError new Error("Failed to parse incoming JSON. #{ex.message}"), 'EJSON' if @stream @emit 'error', ex # we must collect errors even in stream mode errors.push ex else row = {} row[Object.keys(columns)[0]] = chunksBuffer.join '' chunksBuffer = null if @verbose @_log util.inspect row @_log "---------- --------------------" if @stream @emit 'row', row else recordset.push row unless @stream # all rows of current recordset loaded Object.defineProperty recordset, 'columns', enumerable: false value: columns Object.defineProperty recordset, 'toTable', enumerable: false value: -> Table.fromRecordset @ recordsets.push recordset recordset = [] columns = {} req.on 'doneProc', (rowCount, more, returnStatus) => returnValue = returnStatus req.on 'returnValue', (parameterName, value, metadata) => if @verbose if value is tds.TYPES.Null @_log " output: @#{parameterName}, null" else @_log " output: @#{parameterName}, #{@parameters[parameterName].type.declaration.toLowerCase()}, #{value}" @parameters[parameterName].value = if value is tds.TYPES.Null then null else value for name, param of @parameters if @verbose if param.value is tds.TYPES.Null @_log " #{if param.io is 1 then " input" else "output"}: @#{param.name}, null" else @_log " #{if param.io is 1 then " input" else "output"}: @#{param.name}, #{param.type.declaration.toLowerCase()}, #{param.value}" if param.io is 1 req.addParameter param.name, getTediousType(param.type), parameterCorrection(param.value), {length: param.length, scale: param.scale, precision: param.precision} else req.addOutputParameter param.name, getTediousType(param.type), parameterCorrection(param.value), {length: param.length, scale: param.scale, precision: param.precision} if @verbose then @_log "---------- response -----------" connection.callProcedure req else if connection then @_release connection callback? err ### Cancel currently executed request. ### cancel: -> if @_cancel then return @_cancel() true return { Connection: TediousConnection Transaction: TediousTransaction Request: TediousRequest fix: -> # there is nothing to fix in this driver }