@firstfleet/ffmsnodesqlv8
Version:
MSSQL Task and Promise Wrapper Library
356 lines (322 loc) • 10.9 kB
JavaScript
/**
* Created by Stephen on 28/06/2017.
*/
// the main work horse that manages a query from start to finish by interacting with the c++
const readerModule = ((() => {
function DriverRead (cppDriver, queue) {
const native = cppDriver
const workQueue = queue
let useUTC = true
function setUseUTC (utc) {
useUTC = utc
}
// invokeObject.begin(queryId, query, params, onInvoke)
function getQuery (notify, query, params, invokeObject, callback) {
const q = new Query(notify, workQueue, query, params, invokeObject, callback)
notify.setQueryWorker(q)
return q
}
class Query {
constructor (notify, queue, query, params, invokeObject, callback) {
let meta
let rows = []
let outputParams = []
const queryId = notify.getQueryId()
let queryRowIndex = 0
let batchRowIndex = 0
let batchData = null
let running = true
let paused = false
let done = false
let infoFromNextResult = false
const rowBatchSize = 50 /* ignored for prepared statements */
function isInfo (err) {
return err && err.sqlstate && err.sqlstate.length >= 2 && err.sqlstate.substring(0, 2) === '01'
}
/* route non critical info messages to its own event to prevent streams based readers from halting */
const routeStatementError = (errorsAndInfo, callback, notify) => {
if (!Array.isArray(errorsAndInfo)) {
errorsAndInfo = [errorsAndInfo]
}
let i = 0
const onlyErrors = errorsAndInfo.reduce((agg, latest) => {
if (!isInfo(latest)) {
agg.push(latest)
}
return agg
}, [])
const errorCount = onlyErrors.length
errorsAndInfo.forEach(err => {
const info = isInfo(err)
if (callback && !info) {
const more = i < errorsAndInfo.length - 1
callback(err, null, more)
} else {
const ev = info ? 'info' : 'error'
if (notify) {
const more = i < errorCount - 1
notify.emit(ev, err, more)
} else {
throw new Error(err)
}
}
++i
})
}
function nativeGetRows (queryId, rowBatchSize) {
return new Promise((resolve, reject) => {
native.readColumn(queryId, rowBatchSize, (e, res) => {
setImmediate(() => {
if (e) {
reject(e)
} else {
resolve(res)
}
})
})
})
}
function close () {
running = false
workQueue.nextOp()
}
function emitDone () {
setImmediate(() => {
if (done) return
done = true
notify.emit('done', queryId)
})
}
function dispatchInfoReturnErrors (e) {
const infoMessages = []
const errorMessages = []
if (e && Array.isArray(e)) {
e.forEach(errorOrInfo => {
if (isInfo(errorOrInfo)) {
infoMessages.push(errorOrInfo)
} else {
errorMessages.push(errorOrInfo)
}
})
}
if (errorMessages.length > 0) {
return errorMessages
} else if (infoMessages.length > 0) {
routeStatementError(infoMessages, callback, notify, false)
}
return []
}
function nativeNextResult (queryId) {
return new Promise((resolve, reject) => {
infoFromNextResult = false
native.nextResult(queryId, (e, res) => {
setImmediate(() => {
// may contain info messages e.g. raised by PRINT statements - do not want to reject these
const errorMessages = e ? dispatchInfoReturnErrors(e) : []
if (errorMessages.length > 0) {
reject(errorMessages)
} else {
infoFromNextResult = e != null && Array.isArray(e) && e.length > 0
resolve(res)
}
})
})
})
}
function beginQuery (queryId) {
return new Promise((resolve, reject) => {
invokeObject.begin(queryId, query, params, (e, columnDefinitions, procOutputOrMore) => {
setImmediate(() => {
if (e && !procOutputOrMore) {
reject(e)
} else {
resolve({
warning: e,
columnDefinitions: columnDefinitions,
procOutput: procOutputOrMore
})
}
})
})
})
}
// console.log('fetch ', queryId)
const dispatchRows = (results) => {
if (!results) { return }
if (paused) return
const resultRows = results.data
if (!resultRows) { return }
const numberRows = resultRows.length
while (batchRowIndex < numberRows) {
if (paused) {
break
}
const driverRow = resultRows[batchRowIndex]
notify.emit('row', queryRowIndex)
batchRowIndex++
queryRowIndex++
let currentRow
if (callback) {
currentRow = []
rows[rows.length] = currentRow
}
for (let column = 0; column < driverRow.length; ++column) {
let rowColumn = driverRow[column]
if (rowColumn && useUTC === false) {
if (meta[column].type === 'date') {
rowColumn = new Date(rowColumn.getTime() - rowColumn.getTimezoneOffset() * -60000)
}
}
if (callback) {
currentRow[column] = rowColumn
}
notify.emit('column', column, rowColumn, false)
}
}
}
function rowsCompleted (results, more) {
invokeObject.end(notify, outputParams, (err, r, freeMore, op) => {
if (callback) {
callback(err, r, freeMore, op)
}
if (!freeMore) {
emitDone()
}
}, results, more)
}
function rowsAffected (nextResultSetInfo) {
const rowCount = nextResultSetInfo.rowCount
const preRowCount = nextResultSetInfo.preRowCount
const moreResults = !nextResultSetInfo.endOfResults || infoFromNextResult
notify.emit('rowcount', preRowCount)
const state = {
meta: null,
rowcount: rowCount
}
rowsCompleted(state, moreResults)
}
function end (err) {
invokeObject.end(notify, outputParams, () => {
if (!Array.isArray(err)) {
err = [err]
}
routeStatementError(err, callback, notify)
}, null, false)
close()
}
function moveToNextResult (nextResultSetInfo) {
setImmediate(() => {
if (!meta) {
rowsCompleted(
{
meta: meta,
rows: rows
},
!nextResultSetInfo.endOfResults)
} else if (infoFromNextResult) {
rowsAffected(nextResultSetInfo)
nextResult()
return
} else if (meta && meta.length === 0) {
// handle the just finished result reading
// if there was no metadata, then pass the row count (rows affected)
rowsAffected(nextResultSetInfo)
} else {
rowsCompleted(
{
meta: meta,
rows: rows
},
!nextResultSetInfo.endOfResults)
}
// reset for the next resultset
meta = nextResultSetInfo.meta
if (!meta) {
nextResult()
return
}
rows = []
if (nextResultSetInfo.endOfResults) {
close()
} else {
// if this is just a set of rows
if (meta.length > 0) {
notify.emit('meta', meta)
// kick off reading next set of rows
dispatch()
} else {
nextResult()
}
}
})
}
function dispatch () {
if (!running) return
if (paused) return // will come back at some later stage
nativeGetRows(queryId, rowBatchSize).then(d => {
batchRowIndex = 0
batchData = d
dispatchRows(d)
if (!d.end_rows) {
dispatch()
} else {
nextResult()
}
}).catch(err => {
end(err)
})
}
const nextResult = () => {
infoFromNextResult = false
nativeNextResult(queryId).then(nextResultSetInfo => {
moveToNextResult(nextResultSetInfo)
}).catch(err => {
end(err)
})
}
function begin () {
beginQuery(queryId, query, params).then(res => {
if (res.warning) {
routeStatementError(res.warning, callback, notify)
}
outputParams = res.outputParams
meta = res.columnDefinitions
if (meta.length > 0) {
notify.emit('meta', meta)
dispatch()
} else {
nextResult()
}
}).catch(err => {
end(err)
})
notify.emit('submitted', query, params)
}
function pause () {
if (paused) return
paused = true
queue.park(notify.getOperation())
}
function resume () {
if (!paused) return
queue.resume(notify.getOperation())
paused = false
dispatchRows(batchData)
dispatch()
}
this.begin = begin
this.pause = pause
this.resume = resume
}
}
return {
setUseUTC: setUseUTC,
getQuery: getQuery
}
}
return {
DriverRead: DriverRead
}
})())
exports.readerModule = readerModule