UNPKG

mssql2

Version:

Microsoft SQL Server client for Node.js (fork)

568 lines (428 loc) 16.7 kB
{Pool} = require 'generic-pool' msnodesql = require 'msnodesqlv8' util = require 'util' {TYPES, declare} = require('./datatypes') UDT = require('./udt').PARSERS ISOLATION_LEVEL = require('./isolationlevel') DECLARATIONS = require('./datatypes').DECLARATIONS EMPTY_BUFFER = new Buffer(0) JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B' XML_COLUMN_ID = 'XML_F52E2B61-18A1-11d1-B105-00805F49916B' CONNECTION_STRING_PORT = 'Driver={SQL Server Native Client 11.0};Server={#{server},#{port}};Database={#{database}};Uid={#{user}};Pwd={#{password}};Trusted_Connection={#{trusted}};' CONNECTION_STRING_NAMED_INSTANCE = 'Driver={SQL Server Native Client 11.0};Server={#{server}\\#{instance}};Database={#{database}};Uid={#{user}};Pwd={#{password}};Trusted_Connection={#{trusted}};' ### @ignore ### castParameter = (value, type) -> unless value? if type is TYPES.Binary or type is TYPES.VarBinary or type is TYPES.Image # msnodesql has some problems with NULL values in those types, so we need to replace it with empty buffer return EMPTY_BUFFER 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 ### createColumns = (metadata) -> out = {} for column, index in metadata out[column.name] = index: index name: column.name length: column.size type: DECLARATIONS[column.sqlType] if column.udtType? out[column.name].udt = name: column.udtType if DECLARATIONS[column.udtType] out[column.name].type = DECLARATIONS[column.udtType] 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." ### @ignore ### valueCorrection = (value, metadata) -> if metadata.sqlType is 'time' and value? value.setFullYear(1970) value else if metadata.sqlType is 'udt' and value? if UDT[metadata.udtType] UDT[metadata.udtType] value else value else value ### @ignore ### module.exports = (Connection, Transaction, Request, ConnectionError, TransactionError, RequestError) -> class MsnodesqlConnection extends Connection pool: null connect: (config, callback) -> defaultConnectionString = CONNECTION_STRING_PORT if config.options.instanceName? defaultConnectionString = CONNECTION_STRING_NAMED_INSTANCE cfg = conn_str: config.connectionString ? defaultConnectionString conn_timeout: (config.connectionTimeout ? config.timeout ? 15000) / 1000 # config.timeout deprecated in 0.6.0 cfg.conn_str = cfg.conn_str.replace new RegExp('#{([^}]*)}', 'g'), (p) -> key = p.substr(2, p.length - 3) if key is 'instance' return config.options.instanceName else if key is 'trusted' return if config.options.trustedConnection then 'Yes' else 'No' else return config[key] ? '' cfg_pool = name: 'mssql' max: 10 min: 0 idleTimeoutMillis: 30000 create: (callback) => msnodesql.open cfg, (err, c) => 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?.close() 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 MsnodesqlTransaction extends Transaction begin: (callback) -> @connection.pool.acquire (err, connection) => if err then return callback err @_pooledConnection = connection @request()._dedicated(@_pooledConnection).query "set transaction isolation level #{isolationLevelDeclaration(@isolationLevel)};begin tran;", callback commit: (callback) -> @request()._dedicated(@_pooledConnection).query 'commit tran', (err) => @connection.pool.release @_pooledConnection @_pooledConnection = null callback err rollback: (callback) -> @request()._dedicated(@_pooledConnection).query 'rollback tran', (err) => @connection.pool.release @_pooledConnection @_pooledConnection = null callback err class MsnodesqlRequest extends Request batch: (batch, callback) -> MsnodesqlRequest::query.call @, batch, callback 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() @_acquire (err, connection) => unless err if @verbose then @_log "-------- sql bulk load --------\n table: #{table.name}" done = (err, rowCount) => if err if 'string' is typeof err.sqlstate and err.sqlstate.toLowerCase() is '08s01' connection.hasError = true e = RequestError err if (/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec err.message e.message = RegExp.$1 e.code = 'EREQUEST' if @verbose and not @nested @_log " error: #{e}" if @verbose elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_release connection if e callback? e else callback? null, table.rows.length go = => tm = connection.tableMgr() tm.bind table.path.replace(/\[|\]/g, ''), (mgr) => if mgr.columns.length is 0 return done new RequestError("Table was not found on the server.", "ENAME") rows = [] for row in table.rows item = {} for col, index in table.columns item[col.name] = row[index] rows.push item mgr.insertRows rows, done if table.create if table.temporary objectid = "tempdb..[#{table.name}]" else objectid = table.path if @verbose elapsed = Date.now() - started @_log " message: attempting to create table #{table.path} if not exists" req = connection.queryRaw "if object_id('#{objectid.replace(/'/g, '\'\'')}') is null #{table.declare()}", (err) -> if err then return done err go() else go() query: (command, callback) -> 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 row = null columns = null recordset = null recordsets = [] started = Date.now() handleOutput = false isChunkedRecordset = false chunksBuffer = null # nested = function is called by this.execute unless @nested input = ("@#{param.name} #{declare(param.type, param)}" for name, param of @parameters) sets = ("set @#{param.name}=?" for name, param of @parameters when param.io is 1) output = ("@#{param.name} as '#{param.name}'" for name, param of @parameters when param.io is 2) if input.length then command = "declare #{input.join ','};#{sets.join ';'};#{command};" if output.length command += "select #{output.join ','};" handleOutput = true @_acquire (err, connection) => unless err if @verbose and not @nested then @_log "---------- sql query ----------\n query: #{command}" req = connection.queryRaw command, (castParameter(param.value, param.type) for name, param of @parameters when param.io is 1) if @verbose and not @nested then @_log "---------- response -----------" req.on 'meta', (metadata) => if row if isChunkedRecordset if columns[0].name is JSON_COLUMN_ID and @connection.config.parseJSON is true try row = JSON.parse chunksBuffer.join('') if not @stream then recordsets[recordsets.length - 1][0] = row catch ex row = null ex = RequestError new Error("Failed to parse incoming JSON. #{ex.message}"), 'EJSON' if @stream @emit 'error', ex else console.error ex else row[columns[0].name] = chunksBuffer.join '' chunksBuffer = null if @verbose @_log util.inspect(row) @_log "---------- --------------------" unless row["___return___"]? # row with ___return___ col is the last row if @stream then @emit 'row', row row = null columns = metadata recordset = [] Object.defineProperty recordset, 'columns', enumerable: false value: createColumns(metadata) isChunkedRecordset = false if metadata.length is 1 and metadata[0].name in [JSON_COLUMN_ID, XML_COLUMN_ID] isChunkedRecordset = true chunksBuffer = [] if @stream unless recordset.columns["___return___"]? @emit 'recordset', recordset.columns else recordsets.push recordset req.on 'row', (rownumber) => if row if isChunkedRecordset then return if @verbose @_log util.inspect(row) @_log "---------- --------------------" unless row["___return___"]? # row with ___return___ col is the last row if @stream then @emit 'row', row row = {} unless @stream recordset.push row req.on 'column', (idx, data, more) => if isChunkedRecordset chunksBuffer.push data else data = valueCorrection(data, columns[idx]) exi = row[columns[idx].name] if exi? if exi instanceof Array exi.push data else row[columns[idx].name] = [exi, data] else row[columns[idx].name] = data req.on 'rowcount', (count) => @rowsAffected += count if count > 0 req.once 'error', (err) => if 'string' is typeof err.sqlstate and err.sqlstate.toLowerCase() is '08s01' connection.hasError = true e = RequestError err if (/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec err.message e.message = RegExp.$1 e.code = 'EREQUEST' if @verbose and not @nested elapsed = Date.now() - started @_log " error: #{err}" @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_release connection callback? e req.once 'done', => unless @nested if row if isChunkedRecordset if columns[0].name is JSON_COLUMN_ID and @connection.config.parseJSON is true try row = JSON.parse chunksBuffer.join('') if not @stream then recordsets[recordsets.length - 1][0] = row catch ex row = null ex = RequestError new Error("Failed to parse incoming JSON. #{ex.message}"), 'EJSON' if @stream @emit 'error', ex else console.error ex else row[columns[0].name] = chunksBuffer.join '' chunksBuffer = null if @verbose @_log util.inspect(row) @_log "---------- --------------------" unless row["___return___"]? # row with ___return___ col is the last row if @stream then @emit 'row', row # 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 elapsed = Date.now() - started @_log " duration: #{elapsed}ms" @_log "---------- completed ----------" @_release connection if @stream callback null, if @nested then row else null else callback? null, if @multiple or @nested then recordsets else recordsets[0] 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}=?" 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) MsnodesqlRequest::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: -> false # Request canceling is not implemented by msnodesql driver. return { Connection: MsnodesqlConnection Transaction: MsnodesqlTransaction Request: MsnodesqlRequest fix: -> # there is nothing to fix in this driver }