UNPKG

@firstfleet/ffmsnodesqlv8

Version:
356 lines (322 loc) 10.9 kB
/** * Created by Stephen on 28/06/2017. */ // the main work horse that manages a query from start to finish by interacting with the c++ 'use strict' 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