UNPKG

mssql

Version:

Microsoft SQL Server client for Node.js.

1,264 lines (1,118 loc) • 46.8 kB
'use strict' const msnodesql = require('msnodesqlv8') const debug = require('debug')('mssql:msv8') const BaseRequest = require('../base/request') const RequestError = require('../error/request-error') const { IDS, objectHasProperty, assertSafeColumnName, assertSafeTypeName, assertSafeTypeSize, assertSafeProcedureName } = require('../utils') const { TYPES, DECLARATIONS, declare } = require('../datatypes') const { PARSERS: UDT } = require('../udt') const Table = require('../table') const { valueHandler } = require('../shared') const JSON_COLUMN_ID = 'JSON_F52E2B61-18A1-11d1-B105-00805F49916B' const XML_COLUMN_ID = 'XML_F52E2B61-18A1-11d1-B105-00805F49916B' const EMPTY_BUFFER = Buffer.alloc(0) // msnodesqlv8 fetches a result set's rows this many at a time. It is only safe to pause once // it has handed over every row of the batch it fetched: paused part way through a result set's // last, short batch, it moves on to the next result set, or ends the query, with the rest of // that batch still to come. const DRIVER_ROW_BATCH_SIZE = 50 // String() converts a Symbol, which only fails to convert implicitly; an object without a // working toString falls back to its tag const thrownMessage = function (value) { try { return String(value) } catch (e) { return Object.prototype.toString.call(value) } } // msnodesqlv8 >= 3 keeps the reader of a query on it, and the reader stops running as soon as // the query ends, a few ticks before msnodesqlv8 reports the end with 'done' or 'free' const stopped = function (query) { return query.queryWorker?.running === false } const castParameter = function (value, type) { if (value == null) { if ((type === TYPES.Binary) || (type === TYPES.VarBinary) || (type === 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) { case TYPES.VarChar: case TYPES.NVarChar: case TYPES.Char: case TYPES.NChar: case TYPES.Xml: case TYPES.Text: case TYPES.BigInt: case TYPES.NText: if ((typeof value !== 'string') && !(value instanceof String)) { value = value.toString() } break case TYPES.Int: case TYPES.TinyInt: case TYPES.SmallInt: if ((typeof value !== 'number') && !(value instanceof Number)) { value = parseInt(value) if (isNaN(value)) { value = null } } break case TYPES.Float: case TYPES.Real: case TYPES.Decimal: case TYPES.Numeric: case TYPES.SmallMoney: case TYPES.Money: if ((typeof value !== 'number') && !(value instanceof Number)) { value = parseFloat(value) if (isNaN(value)) { value = null } } break case TYPES.Bit: if ((typeof value !== 'boolean') && !(value instanceof Boolean)) { value = Boolean(value) } break case TYPES.DateTime: case TYPES.SmallDateTime: case TYPES.DateTimeOffset: case TYPES.Date: if (!(value instanceof Date)) { value = new Date(value) } break case TYPES.Binary: case TYPES.VarBinary: case TYPES.Image: if (!(value instanceof Buffer)) { value = Buffer.from(value.toString()) } break case TYPES.TVP: value = msnodesql.TvpFromTable(value) break } return value } // ODBC names the type of an identity column after the type it holds, as 'int identity', and // gives decimal and numeric the brackets of their precision, as 'decimal() identity' const IDENTITY_SQL_TYPE = /^(tinyint|smallint|int|bigint|decimal|numeric)(?:\(\))? identity$/ // msnodesqlv8 describes a column by the name ODBC gives its type. An identity column is described // by the type it holds instead, so that it is described, and its values read, as any other column // of that type. const describeColumn = function (column) { const identity = typeof column.sqlType === 'string' ? IDENTITY_SQL_TYPE.exec(column.sqlType) : null return identity ? { ...column, sqlType: identity[1], identity: true } : column } const createColumns = function (metadata, arrayRowMode) { let out = {} if (arrayRowMode) out = [] for (let index = 0, length = metadata.length; index < length; index++) { const column = metadata[index] const colName = column.name const outColumn = { index, name: column.name, length: column.size, type: DECLARATIONS[column.sqlType], nullable: column.nullable, identity: column.identity === true } if (column.udtType != null) { outColumn.udt = { name: column.udtType } if (DECLARATIONS[column.udtType]) { outColumn.type = DECLARATIONS[column.udtType] } } if (arrayRowMode) { out.push(outColumn) } else { out[colName] = outColumn } } return out } const numericStringSqlTypes = new Set(['int', 'smallint', 'tinyint', 'float', 'real', 'numeric', 'decimal', 'money', 'smallmoney']) const valueCorrection = function (value, metadata) { // a number msnodesqlv8 hands over as a string is read as a number before a value handler is // given it, as it is with the tedious driver if (typeof value === 'string') { if (numericStringSqlTypes.has(metadata.sqlType)) { value = Number(value) } else if (metadata.sqlType === 'sql_variant' && value !== '' && !isNaN(Number(value))) { value = Number(value) } } const type = metadata && objectHasProperty(metadata, 'sqlType') && objectHasProperty(DECLARATIONS, metadata.sqlType) ? DECLARATIONS[metadata.sqlType] : null if (type && valueHandler.has(type)) { return valueHandler.get(type)(value) } else if ((metadata.sqlType === 'time') && (value != null)) { value.setFullYear(1970) return value } else if ((metadata.sqlType === 'udt') && (value != null)) { if (UDT[metadata.udtType]) { return UDT[metadata.udtType](value) } else { return value } } else { return value } } class Request extends BaseRequest { _batch (batch, callback) { this._isBatch = true this._query(batch, callback) } _bulk (table, options, callback) { super._bulk(table, options, err => { if (err) return callback(err) try { table._makeBulk() // Checked before a connection is borrowed, so that a rejected name cannot leave // the pool short of one. table.columns.forEach(col => { assertSafeColumnName(col.name) // This driver hands the column definition to its table manager rather than writing // it into SQL, so a size never reaches a statement here. It is checked all the same, // so that a column either works on both drivers or on neither. assertSafeTypeSize(col.length) assertSafeTypeSize(col.precision) assertSafeTypeSize(col.scale) }) // checked here too, so an ordering key is accepted or rejected the same way on // either driver even though this one does not build the statement itself if (options && options.order) { Object.keys(options.order).forEach(key => assertSafeTypeName(key)) } } catch (e) { return callback(new RequestError(e, 'EREQUEST')) } if (!table.name) { return setImmediate(callback, new RequestError('Table name must be specified for bulk insert.', 'ENAME')) } if (table.name.charAt(0) === '@') { return setImmediate(callback, new RequestError("You can't use table variables for bulk insert.", 'ENAME')) } this.parent.acquire(this, (err, connection, config) => { let hasReturned = false if (!err) { debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this)) if (this.canceled) { debug('request(%d): canceled', IDS.get(this)) this.parent.release(connection) return callback(new RequestError('Canceled.', 'ECANCEL')) } // Disable useNumericString during bulk operations to avoid // msnodesqlv8 SQL generation bugs with its table manager connection.setUseNumericString(false) const done = (err, rowCount) => { if (hasReturned) { return } hasReturned = true connection.setUseNumericString(true) if (err) { if ((typeof err.sqlstate === 'string') && (err.sqlstate.toLowerCase() === '08s01')) { connection.hasError = true } // a rejected identifier keeps its own code; anything else is a driver // error, whose numeric code must not become the public one const rejectedIdentifier = err.code === 'EINJECT' err = new RequestError(err) err.code = rejectedIdentifier ? 'EINJECT' : 'EREQUEST' } this.parent.release(connection) // msnodesqlv8's table manager calls `done` from within promise chains, which would // catch what the caller's callback throws and pass it back here as the driver's // error, to be dropped. The caller is called back outside them, so that what its // callback throws reaches the process as it would from any other callback. if (err) { process.nextTick(callback, err) } else { process.nextTick(callback, null, table.rows.length) } } // msnodesqlv8 throws, rather than calls back, for a query on a connection it has // closed, so what it throws is handed to `done` as well, which releases the connection const attempt = run => { try { run() } catch (e) { done(e) } } const go = () => { const tm = connection.tableMgr() // the table manager reports a failed lookup as (null, err), not error first return tm.bind(table.path.replace(/\[|\]/g, ''), (mgr, err) => { if (err) { return done(err) } if (mgr.columns.length === 0) { return done(new RequestError('Table was not found on the server.', 'ENAME')) } const rows = [] try { for (const col of table.columns) { // re-checked here for the same reason as the name below, in case the // definition changed since it was checked above assertSafeTypeSize(col.length) assertSafeTypeSize(col.precision) assertSafeTypeSize(col.scale) } for (const row of Array.from(table.rows)) { const item = {} for (let index = 0; index < table.columns.length; index++) { const col = table.columns[index] // this driver hands the name to its table manager rather than writing // it into SQL, but it is checked here all the same: a name either // works on both drivers or on neither item[assertSafeColumnName(col.name)] = row[index] } rows.push(item) } } catch (e) { return done(e) } mgr.insertRows(rows, done) }) } if (table.create) { let objectid if (table.temporary) { objectid = `tempdb..[${table.name}]` } else { objectid = table.path } // A connection is held from here on, so a column name that changed since it // was checked above must be reported through `done`, which releases it. let statement try { statement = { query_str: `if object_id('${objectid.replace(/'/g, '\'\'')}') is null ${table.declare()}`, query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported) } } catch (e) { return done(e) } return attempt(() => connection.queryRaw(statement, function (err) { if (err) { return done(err) } attempt(go) })) } else { attempt(go) } } else { callback(err) } }) }) } _query (command, callback, bound) { super._query(command, err => { if (err) return callback(err) if (command.length === 0) { return callback(null, []) } const recordsets = [] const recordsetcolumns = [] const errors = [] const errorHandlers = {} const output = {} const rowsAffected = [] let hasReturned = false let row = null let columns = null let recordset = null let handleOutput = false let isChunkedRecordset = false let chunksBuffer = null let terminated = null // set while the error just collected is being dispatched, along with whatever came with it let failing = false let cancelRequested = false // 'done' or 'free' ends the request. msnodesqlv8 >= 5.6 emits 'done' as soon as a cancel is // through, and the query can go on emitting after it, so the listeners ignore anything that // follows let finished = false // A stream goes on receiving rows after it is paused, until the driver reaches the end of a // full batch (see DRIVER_ROW_BATCH_SIZE). What the stream emits waits here while it is // paused, and is emitted in the order it arrived once it is resumed. const pending = [] let rowsInRecordset = 0 let driverPaused = false // a cancel of a paused stream waits for the driver to reach a point where it can be paused let cancelDeferred = false // msnodesqlv8 has freed the statement, which it does without 'done' after an error ends the query let freed = false let resuming = false // a refused cancel undoes only the latest one let cancelAttempts = 0 const emitInOrder = (isRow, emit) => { if (this.stream && (this.paused || pending.length)) { pending.push({ isRow, emit }) } else { emit() } } // once a cancel is through, what is waiting is emitted at once, but for the rows of a // paused stream, which are dropped const emitPendingOnCancel = () => { // decided once, so that a listener that resumes the stream part way through cannot let // the rows of the cancelled stream through const dropRows = this.paused for (const { isRow, emit } of pending.splice(0)) { if (!isRow || !dropRows) emit() } } const handleError = (req, connection, info, moreErrors) => { // a cancel is reported from 'done'. msnodesqlv8 >= 5.6 also sends an 'Operation canceled' // error after a cancel, and one that arrives before 'done' is dropped here if (cancelRequested && info.sqlstate === 'HY008') { return } const doReturn = !moreErrors const err = new RequestError(info, 'EREQUEST') err.code = 'EREQUEST' if (this.stream) { emitInOrder(false, () => this.emit('error', err)) } else { if (doReturn && !hasReturned) { if (req) { for (const event in errorHandlers) { req.removeListener(event, errorHandlers[event]) } } if (connection) { this.parent.release(connection) delete this._cancel debug('request(%d): failed', IDS.get(this), err) } if (errors.length) { err.precedingErrors = errors.splice(0) } hasReturned = true callback(err) } } // we must collect errors even in stream mode errors.push(err) if (!failing) { failing = true queueMicrotask(() => { failing = false }) } } // nested = function is called by this.execute if (!this._nested) { const input = [] try { for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] // this callback is dispatched on its own tick, so a rejected declaration has // to be handed back rather than thrown input.push(`@${param.name} ${declare(param.type, param)}`) } } catch (e) { return callback(new RequestError(e, 'EREQUEST')) } const sets = [] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] // an output's initial value is bound with the inputs below, so it needs a marker too; // an output given no value is null, and 0, '' and false are values like any other if (param.io === 1 || (param.io === 2 && param.value != null)) { sets.push(`set @${param.name}=?`) } } const output = [] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { output.push(`@${param.name} as '${param.name}'`) } } if (input.length) command = `declare ${input.join(',')};${sets.join(';')};${command};` if (output.length) { command += `select ${output.join(',')};` handleOutput = true } } // values are bound to the markers by position, so a caller that writes its own // markers passes the parameters in the order it wrote them. They are converted before // a connection is borrowed, so that a value that cannot be converted is handed back // with nothing to release, rather than thrown while a connection is held. const params = [] let converting try { if (bound) { for (const param of bound) { converting = param.name params.push(castParameter(param.value, param.type)) } } else { for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 1 || (param.io === 2 && param.value != null)) { converting = param.name params.push(castParameter(param.value, param.type)) } } } } catch (e) { // what was thrown need not be an Error, nor convert to a string implicitly, and may // carry a code of its own const err = new RequestError(e instanceof Error ? e : thrownMessage(e), 'EPARAM') err.code = 'EPARAM' err.message = `Validation failed for parameter '${converting}'. ${err.message}` return callback(err) } this.parent.acquire(this, (err, connection, config) => { if (err) return callback(err) debug('connection(%d): borrowed to request #%d', IDS.get(connection), IDS.get(this)) if (this.canceled) { debug('request(%d): canceled', IDS.get(this)) this.parent.release(connection) return callback(new RequestError('Canceled.', 'ECANCEL')) } debug('request(%d): query', IDS.get(this), command) // msnodesqlv8 throws, rather than calls back, for a query on a connection it has closed let req try { req = connection.queryRaw({ query_str: command, query_timeout: (this.overrides.requestTimeout ?? config.requestTimeout) / 1000 // msnodesqlv8 timeouts are in seconds (<1 second not supported) }, params) } catch (e) { this.parent.release(connection) const err = new RequestError(e, 'EREQUEST') err.code = 'EREQUEST' return callback(err) } this._setCurrentRequest(req) // msnodesqlv8 does not check that a query is still running when it is paused or resumed. // Pausing and resuming one that has ended puts it back on its connection's work queue, // where it holds up whatever that connection is sent next, and resuming it can set it // reading again from a statement that is gone, or crash the process. So once msnodesqlv8 // reports that the query has ended, the request no longer pauses, resumes or cancels it, // before anything the request emits from then on is handled. Before then it leaves the // query alone while an error that may end the batch is being handled and, from msnodesqlv8 // 3, once the query's reader has stopped let detached = false let settingAside = false let cancelHeld = false const detach = () => { detached = true } const reachable = () => !detached && !settingAside && !stopped(req) const cancelDriver = (attempt = cancelAttempts) => { req.cancelQuery(err => { if (err) { debug('request(%d): failed to cancel', IDS.get(this), err) // msnodesqlv8 < 5.6 refuses to cancel a query that is neither paused nor polling, // and the query runs to the end; if it reached the end of a batch meanwhile, a // stream that is no longer paused resumes it if (!hasReturned && attempt === cancelAttempts) { cancelRequested = false this._resumeStream() } } // this fixes an issue where paused connections don't emit a done event; a query whose // cancel was refused goes on, though it may have paused at the end of a batch since try { if (!err && req.isPaused()) req.emit('done') } catch (err) { // do nothing } }) } // every version of msnodesqlv8 cancels a paused query; it is paused here only where it stops // for a pause, with nothing of its own under way const pauseAndCancelDriver = () => { cancelDeferred = false // msnodesqlv8 5.0 to 5.5 send a query one cancel, which a cancel made before the query // started has used up and they refused, so the query runs on if (typeof req.getCancelSent === 'function' && req.getCancelSent()) { cancelRequested = false return false } driverPaused = true req.pauseQuery() cancelDriver() return true } // called with each row once it is complete const pauseDriverAtEndOfBatch = () => { if (driverPaused || rowsInRecordset % DRIVER_ROW_BATCH_SIZE !== 0) return if (cancelDeferred && pauseAndCancelDriver()) return if (this.stream && this.paused) { driverPaused = true req.pauseQuery() } } // until the first result set the driver has read nothing, so a stream paused before then, // or before its query starts, holds it back at once this._pauseStream = () => { if (!columns && !driverPaused && !finished && reachable()) { driverPaused = true req.pauseQuery() } } if (this.stream && this.paused) this._pauseStream() this._resumeStream = () => { // a listener that resumes the stream while it is emitting what was waiting leaves the // rest to this loop, so that every listener gets the events in order if (resuming) return resuming = true try { while (pending.length && !this.paused) { pending.shift().emit() } } finally { resuming = false } // cancelled while it emitted what was waiting after the query ended, and then paused, a // stream completes as cancelled straight away, as it does when it is paused first if (finished && cancelRequested && this.paused && pending.length) emitPendingOnCancel() // a query that is being cancelled stays paused, so that the cancel can go through if (!pending.length && !this.paused && driverPaused && !finished && !cancelRequested && reachable()) { driverPaused = false req.resumeQuery() } } const cancel = () => { debug('request(%d): cancel', IDS.get(this)) // a query that has ended has nothing to cancel, nor one whose reader has stopped. The // cancel is held: it is made if msnodesqlv8 goes on to a result set after all, and once // the query is through a paused stream completes as cancelled, without waiting to be // resumed if (detached || stopped(req) || settingAside) { cancelHeld = true return } // newer msnodesqlv8 versions drop a repeat cancel without calling back, so the // outcome of the first one stands if (cancelDeferred || (typeof req.getCancelSent === 'function' && req.getCancelSent())) { return } cancelRequested = true cancelAttempts++ if (!this.stream || !this.paused || driverPaused) { return cancelDriver() } // A paused stream holds the driver back only once it reaches the end of a batch. Until // then the driver goes on reading, and pausing or cancelling it part way through a fetch, // or as it moves to the next result set or ends the query, can leave its queue broken. // So it is paused and cancelled when it next stops, at the end of a batch or the start // of a result set, and a query that ends first, at the latest on the request timeout, // is reported as cancelled. if (freed) return req.emit('done') cancelDeferred = true } this._cancel = cancel req.on('meta', metadata => { if (finished) return // a query that goes on to a result set has not ended: msnodesqlv8 can give the last error // of a statement that fails first more=false, and carry on to the statements after it if (detached) { detached = false if (cancelHeld) { cancelHeld = false cancel() } // a stream resumed meanwhile left the query held back. msnodesqlv8 fetches as soon as // it is resumed, and again once 'meta' is handled, so the query is resumed a tick // later, once it has stopped at the result set if (!this.paused && driverPaused && !cancelRequested) setImmediate(this._resumeStream) } if (row) { if (isChunkedRecordset) { const concatenatedChunks = chunksBuffer.join('') if ((columns[0].name === JSON_COLUMN_ID) && (config.parseJSON === true)) { try { if (concatenatedChunks === '') { row = null } else { row = JSON.parse(concatenatedChunks) } if (!this.stream) { recordsets[recordsets.length - 1][0] = row } } catch (ex) { row = null const ex2 = new RequestError(`Failed to parse incoming JSON. ${ex.message}`, 'EJSON') if (this.stream) { emitInOrder(false, () => this.emit('error', ex2)) } else { console.error(ex2) } } } else { row[columns[0].name] = concatenatedChunks } chunksBuffer = null if (row && row.___return___ == null) { // row with ___return___ col is the last row const last = row if (this.stream) emitInOrder(true, () => this.emit('row', last)) } } } row = null rowsInRecordset = 0 columns = metadata.map(describeColumn) recordset = [] Object.defineProperty(recordset, 'columns', { enumerable: false, configurable: true, value: createColumns(columns, this.arrayRowMode) }) Object.defineProperty(recordset, 'toTable', { enumerable: false, configurable: true, value (name) { return Table.fromRecordset(this, name) } }) isChunkedRecordset = false if ((metadata.length === 1) && (metadata[0].name === JSON_COLUMN_ID || metadata[0].name === XML_COLUMN_ID)) { isChunkedRecordset = true chunksBuffer = [] } let hasReturnColumn = false if (recordset.columns.___return___ != null) { hasReturnColumn = true } else if (this.arrayRowMode) { for (let i = 0; i < columns.length; i++) { if (columns[i].name === '___return___') { hasReturnColumn = true break } } } if (this.stream) { if (!hasReturnColumn) { const recordsetColumns = recordset.columns emitInOrder(false, () => this.emit('recordset', recordsetColumns)) } } else { recordsets.push(recordset) } if (this.arrayRowMode) recordsetcolumns.push(recordset.columns) // the driver fetches nothing of a result set it is paused at the start of if (cancelDeferred) pauseAndCancelDriver() }) req.on('row', rownumber => { if (finished) return rowsInRecordset++ if (row && isChunkedRecordset) return if (this.arrayRowMode) { row = [] } else { row = {} } if (!this.stream) recordset.push(row) }) req.on('column', (idx, data, more) => { if (finished) return if (isChunkedRecordset) { chunksBuffer.push(data) } else { data = valueCorrection(data, columns[idx]) if (this.arrayRowMode) { row.push(data) } else { const exi = row[columns[idx].name] if (exi != null) { if (exi instanceof Array) { exi.push(data) } else { row[columns[idx].name] = [exi, data] } } else { row[columns[idx].name] = data } } let hasReturnColumn = false if (row && row.___return___ != null) { hasReturnColumn = true } else if (this.arrayRowMode) { for (let i = 0; i < columns.length; i++) { if (columns[i].name === '___return___') { hasReturnColumn = true break } } } if (!hasReturnColumn) { if (this.stream && idx === columns.length - 1) { const complete = row emitInOrder(true, () => this.emit('row', complete)) } } } if (idx === columns.length - 1) pauseDriverAtEndOfBatch() }) req.on('rowcount', rowCount => { if (finished) return rowsAffected.push(rowCount) if (this.stream) { emitInOrder(false, () => this.emit('rowsaffected', rowCount)) } }) req.on('info', (msg, more) => { if (finished) return const parsedMessage = (/^\[Microsoft\]\[SQL Server Native Client 11\.0\](?:\[SQL Server\])?([\s\S]*)$/).exec(msg.message) if (parsedMessage) { msg.message = parsedMessage[1] } const info = { message: msg.message, number: msg.code, state: msg.sqlstate, class: msg.class || 0, lineNumber: msg.lineNumber || 0, serverName: msg.serverName, procName: msg.procName } emitInOrder(false, () => this.emit('info', info)) // query terminated if (msg.code === 3621 && !hasReturned) { if (failing && !more) { // msnodesqlv8 2.0 ends a statement's error with more=true and this message, and then // nothing follows: the error just collected is the one that terminated the statement. // A stream has already emitted it detach() if (!this.stream) { const error = errors.pop() handleError(req, connection, error.originalError || error, false) } } else if (!terminated) { // msnodesqlv8 >= 5.6 emits a failed statement's info messages ahead of its error, and // carries on after it, so any error collected so far is an earlier statement's. The // error still to come, or 'done', settles the request; 'done' falls back to this terminated = msg } } }) req.on('error', errorHandlers.error = (info, moreErrors) => { // a dead link is recorded even after 'done', so the pool does not lend the connection out again if ((typeof info.sqlstate === 'string') && (info.sqlstate.toLowerCase() === '08s01')) { connection.hasError = true } if (!moreErrors) { // an error with nothing more to follow ends the batch detach() } else if (!detached && !settingAside) { // the errors that end a batch are emitted together, all but the last with more=true, // and msnodesqlv8 >= 5.1 releases the statement as soon as they are, so the query is // set aside while they are handled, and a cancel made meanwhile waits until they have // been. If none of them ended the batch it is taken up again and, if the stream was // paused meanwhile before its first result set, paused, unless it is being cancelled. // A stream that is paused holds its events back, so none is resumed meanwhile settingAside = true queueMicrotask(() => { settingAside = false if (cancelHeld) { cancelHeld = false cancel() } if (!detached && !cancelRequested && this.paused && this._pauseStream) this._pauseStream() }) } if (finished) return handleError(req, connection, info, moreErrors) }) req.on('free', () => { freed = true if (cancelDeferred && !finished) { cancelDeferred = false req.emit('done') } }) // msnodesqlv8 emits 'done' when a batch completes, but not when an error ends it; either // way it emits 'free' once it has released the statement. A stream reports its errors as // they arrive rather than settling on them, so without 'free' it would never complete // after such an error, nor give its connection back const complete = () => { finished = true detach() if (cancelHeld && this.stream && this.paused) cancelRequested = true if (hasReturned) { return } if (cancelRequested) { // msnodesqlv8 emits 'done' once a cancel is through: for a paused query in every // version, and for a running one from 5.6. What was read up to then is not a result. // The error listener stays attached and drops the 'Operation canceled' error that can // still arrive after 'done'. hasReturned = true delete this._cancel // released now, as at any 'done', rather than at the statement's later 'free': // msnodesqlv8 queues whatever the connection is sent next behind the cancelled // statement, and a transaction can carry on as soon as the cancel is reported this.parent.release(connection) debug('request(%d): canceled', IDS.get(this)) const err = new RequestError('Canceled.', 'ECANCEL') if (this.stream) { delete this._resumeStream delete this._pauseStream emitPendingOnCancel() // as with tedious, a cancelled stream reports the cancel as an error and still // completes with what was read up to it this.emit('error', err) return callback(null, null, output, rowsAffected, recordsetcolumns) } return callback(err) } if (terminated && !errors.length) { // the statement was terminated, but no error followed to say why handleError(req, connection, terminated, false) } else if (errors.length && !this.stream) { // msnodesqlv8 >= 5.6 reports an error that does not end the batch with more=true and // carries on to 'done', so the request has failed although no error said it was the last const error = errors.pop() handleError(req, connection, error.originalError || error, false) } if (hasReturned) { return } hasReturned = true if (!this._nested) { if (row) { if (isChunkedRecordset) { const concatenatedChunks = chunksBuffer.join('') if ((columns[0].name === JSON_COLUMN_ID) && (config.parseJSON === true)) { try { if (concatenatedChunks === '') { row = null } else { row = JSON.parse(concatenatedChunks) } if (!this.stream) { recordsets[recordsets.length - 1][0] = row } } catch (ex) { row = null const ex2 = new RequestError(`Failed to parse incoming JSON. ${ex.message}`, 'EJSON') if (this.stream) { emitInOrder(false, () => this.emit('error', ex2)) } else { console.error(ex2) } } } else { row[columns[0].name] = concatenatedChunks } chunksBuffer = null if (row && row.___return___ == null) { // row with ___return___ col is the last row const last = row if (this.stream) emitInOrder(true, () => this.emit('row', last)) } } } // do we have output parameters to handle? if (handleOutput && recordsets.length) { const last = recordsets.pop()[0] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { output[param.name] = last[param.name] } } } } this.parent.release(connection) debug('request(%d): completed', IDS.get(this)) if (this.stream) { // the driver is through, but a paused stream completes once it has emitted what is // still waiting. Cancelled before then, it completes as cancelled: at once if it is // paused, dropping its rows, or else once it has emitted the rest this._cancel = () => { cancelRequested = true if (this.paused) emitPendingOnCancel() } const last = this._nested ? row : null emitInOrder(false, () => { delete this._cancel delete this._resumeStream delete this._pauseStream if (cancelRequested) { debug('request(%d): canceled', IDS.get(this)) this.emit('error', new RequestError('Canceled.', 'ECANCEL')) return callback(null, null, output, rowsAffected, recordsetcolumns) } callback(null, last, output, rowsAffected, recordsetcolumns) }) } else { delete this._cancel callback(null, recordsets, output, rowsAffected, recordsetcolumns) } } req.once('done', complete) req.once('free', complete) }) }) } _execute (procedure, callback) { super._execute(procedure, err => { if (err) return callback(err) const params = [] let procedureName try { // this is built into `exec @___return___ = <name>` below and run as SQL text, so it is // checked before it is emitted procedureName = assertSafeProcedureName(procedure) for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { // this callback is dispatched on its own tick, so a rejected declaration has // to be handed back rather than thrown params.push(`@${param.name} ${declare(param.type, param)}`) } else { // An input parameter is bound rather than written into the command, so only an // output parameter's size is declared here and nothing would otherwise check an // input's. It is checked so that a size is accepted or rejected the same way // whichever driver is in use. assertSafeTypeSize(param.length) assertSafeTypeSize(param.precision) assertSafeTypeSize(param.scale) } } } catch (e) { return callback(new RequestError(e, 'EREQUEST')) } // the parameters in the order their markers are written: output initial values are set // before the call and inputs are passed to it, whatever order they were declared in const bound = [] // set output params w/ values const sets = [] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2 && param.value != null) { sets.push(`set @${param.name}=?`) bound.push(param) } } let cmd = `declare ${['@___return___ int'].concat(params).join(', ')};${sets.join(';')};` cmd += `exec @___return___ = ${procedureName} ` const spp = [] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { // output parameter spp.push(`@${param.name}=@${param.name} output`) } else { // input parameter spp.push(`@${param.name}=?`) bound.push(param) } } const params2 = [] for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { params2.push(`@${param.name} as '${param.name}'`) } } cmd += `${spp.join(', ')};` cmd += `select ${['@___return___ as \'___return___\''].concat(params2).join(', ')};` this._nested = true this._query(cmd, (err, recordsets, output, rowsAffected, recordsetcolumns) => { this._nested = false if (err) return callback(err) let last, returnValue if (this.stream) { last = recordsets } else { last = recordsets.pop() if (last) last = last[0] } const lastColumns = recordsetcolumns.pop() if (last && this.arrayRowMode && lastColumns) { let returnColumnIdx = null const parametersNameToLastIdxDict = {} for (let i = 0; i < lastColumns.length; i++) { if (lastColumns[i].name === '___return___') { returnColumnIdx = i } else if (objectHasProperty(this.parameters, lastColumns[i].name)) { parametersNameToLastIdxDict[lastColumns[i].name] = i } } if (returnColumnIdx != null) { returnValue = last[returnColumnIdx] } for (const name in parametersNameToLastIdxDict) { if (!objectHasProperty(parametersNameToLastIdxDict, name)) { continue } const param = this.parameters[name] if (param.io === 2) { output[param.name] = last[parametersNameToLastIdxDict[name]] } } } else { if (last && (last.___return___ != null)) { returnValue = last.___return___ for (const name in this.parameters) { if (!objectHasProperty(this.parameters, name)) { continue } const param = this.parameters[name] if (param.io === 2) { output[param.name] = last[param.name] } } } } if (this.stream) { callback(null, null, output, returnValue, rowsAffected, recordsetcolumns) } else { callback(null, recordsets, output, returnValue, rowsAffected, recordsetcolumns) } }, bound) }) } // the query pauses the driver itself, mostly at the end of a batch (see DRIVER_ROW_BATCH_SIZE) _pause () { super._pause() if (this._pauseStream) { this._pauseStream() } } _resume () { super._resume() if (this._resumeStream) { this._resumeStream() } } } module.exports = Request