pg-native
Version:
A slightly nicer interface to Postgres over node-libpq
332 lines (273 loc) • 8.42 kB
JavaScript
const Libpq = require('libpq')
const EventEmitter = require('events').EventEmitter
const util = require('util')
const assert = require('assert')
const types = require('pg-types')
const buildResult = require('./lib/build-result')
const CopyStream = require('./lib/copy-stream')
const Client = (module.exports = function (config) {
if (!(this instanceof Client)) {
return new Client(config)
}
config = config || {}
EventEmitter.call(this)
this.pq = new Libpq()
this._reading = false
this._read = this._read.bind(this)
// allow custom type conversion to be passed in
this._types = config.types || types
// allow config to specify returning results
// as an array of values instead of a hash
this.arrayMode = config.arrayMode || false
this._resultCount = 0
this._rows = undefined
this._results = undefined
// lazy start the reader if notifications are listened for
// this way if you only run sync queries you wont block
// the event loop artificially
this.on('newListener', (event) => {
if (event !== 'notification') return
this._startReading()
})
this.on('result', this._onResult.bind(this))
this.on('readyForQuery', this._onReadyForQuery.bind(this))
})
util.inherits(Client, EventEmitter)
Client.prototype.connect = function (params, cb) {
this.pq.connect(params, cb)
}
Client.prototype.connectSync = function (params) {
this.pq.connectSync(params)
}
Client.prototype.query = function (text, values, cb) {
let queryFn
if (typeof values === 'function') {
cb = values
}
if (Array.isArray(values)) {
queryFn = () => {
return this.pq.sendQueryParams(text, values)
}
} else {
queryFn = () => {
return this.pq.sendQuery(text)
}
}
this._dispatchQuery(this.pq, queryFn, (err) => {
if (err) return cb(err)
this._awaitResult(cb)
})
}
Client.prototype.prepare = function (statementName, text, nParams, cb) {
const self = this
const fn = function () {
return self.pq.sendPrepare(statementName, text, nParams)
}
self._dispatchQuery(self.pq, fn, function (err) {
if (err) return cb(err)
self._awaitResult(cb)
})
}
Client.prototype.execute = function (statementName, parameters, cb) {
const self = this
const fn = function () {
return self.pq.sendQueryPrepared(statementName, parameters)
}
self._dispatchQuery(self.pq, fn, function (err, rows) {
if (err) return cb(err)
self._awaitResult(cb)
})
}
Client.prototype.getCopyStream = function () {
this.pq.setNonBlocking(true)
this._stopReading()
return new CopyStream(this.pq)
}
// cancel a currently executing query
Client.prototype.cancel = function (cb) {
assert(cb, 'Callback is required')
// result is either true or a string containing an error
const result = this.pq.cancel()
return setImmediate(function () {
cb(result === true ? undefined : new Error(result))
})
}
Client.prototype.querySync = function (text, values) {
if (values) {
this.pq.execParams(text, values)
} else {
this.pq.exec(text)
}
throwIfError(this.pq)
const result = buildResult(this.pq, this._types, this.arrayMode)
return result.rows
}
Client.prototype.prepareSync = function (statementName, text, nParams) {
this.pq.prepare(statementName, text, nParams)
throwIfError(this.pq)
}
Client.prototype.executeSync = function (statementName, parameters) {
this.pq.execPrepared(statementName, parameters)
throwIfError(this.pq)
return buildResult(this.pq, this._types, this.arrayMode).rows
}
Client.prototype.escapeLiteral = function (value) {
return this.pq.escapeLiteral(value)
}
Client.prototype.escapeIdentifier = function (value) {
return this.pq.escapeIdentifier(value)
}
// export the version number so we can check it in node-postgres
module.exports.version = require('./package.json').version
Client.prototype.end = function (cb) {
this._stopReading()
this.pq.finish()
if (cb) setImmediate(cb)
}
Client.prototype._readError = function (message) {
const err = new Error(message || this.pq.errorMessage())
this.emit('error', err)
}
Client.prototype._stopReading = function () {
if (!this._reading) return
this._reading = false
this.pq.stopReader()
this.pq.removeListener('readable', this._read)
}
Client.prototype._consumeQueryResults = function (pq) {
return buildResult(pq, this._types, this.arrayMode)
}
Client.prototype._emitResult = function (pq) {
const status = pq.resultStatus()
switch (status) {
case 'PGRES_FATAL_ERROR':
this._queryError = new Error(this.pq.resultErrorMessage())
break
case 'PGRES_TUPLES_OK':
case 'PGRES_COMMAND_OK':
case 'PGRES_EMPTY_QUERY':
{
const result = this._consumeQueryResults(this.pq)
this.emit('result', result)
}
break
case 'PGRES_COPY_OUT':
case 'PGRES_COPY_BOTH': {
break
}
default:
this._readError('unrecognized command status: ' + status)
break
}
return status
}
// called when libpq is readable
Client.prototype._read = function () {
const pq = this.pq
// read waiting data from the socket
// e.g. clear the pending 'select'
if (!pq.consumeInput()) {
// if consumeInput returns false
// than a read error has been encountered
return this._readError()
}
// check if there is still outstanding data
// if so, wait for it all to come in
if (pq.isBusy()) {
return
}
// load our result object
while (pq.getResult()) {
const resultStatus = this._emitResult(this.pq)
// if the command initiated copy mode we need to break out of the read loop
// so a substream can begin to read copy data
if (resultStatus === 'PGRES_COPY_BOTH' || resultStatus === 'PGRES_COPY_OUT') {
break
}
// if reading multiple results, sometimes the following results might cause
// a blocking read. in this scenario yield back off the reader until libpq is readable
if (pq.isBusy()) {
return
}
}
this.emit('readyForQuery')
let notice = this.pq.notifies()
while (notice) {
this.emit('notification', notice)
notice = this.pq.notifies()
}
}
// ensures the client is reading and
// everything is set up for async io
Client.prototype._startReading = function () {
if (this._reading) return
this._reading = true
this.pq.on('readable', this._read)
this.pq.startReader()
}
const throwIfError = function (pq) {
const err = pq.resultErrorMessage() || pq.errorMessage()
if (err) {
throw new Error(err)
}
}
Client.prototype._awaitResult = function (cb) {
this._queryCallback = cb
return this._startReading()
}
// wait for the writable socket to drain
Client.prototype._waitForDrain = function (pq, cb) {
const res = pq.flush()
// res of 0 is success
if (res === 0) return cb()
// res of -1 is failure
if (res === -1) return cb(pq.errorMessage())
// otherwise outgoing message didn't flush to socket
// wait for it to flush and try again
const self = this
// you cannot read & write on a socket at the same time
return pq.writable(function () {
self._waitForDrain(pq, cb)
})
}
// send an async query to libpq and wait for it to
// finish writing query text to the socket
Client.prototype._dispatchQuery = function (pq, fn, cb) {
this._stopReading()
const success = pq.setNonBlocking(true)
if (!success) return cb(new Error('Unable to set non-blocking to true'))
const sent = fn()
if (!sent) return cb(new Error(pq.errorMessage() || 'Something went wrong dispatching the query'))
this._waitForDrain(pq, cb)
}
Client.prototype._onResult = function (result) {
if (this._resultCount === 0) {
this._results = result
this._rows = result.rows
} else if (this._resultCount === 1) {
this._results = [this._results, result]
this._rows = [this._rows, result.rows]
} else {
this._results.push(result)
this._rows.push(result.rows)
}
this._resultCount++
}
Client.prototype._onReadyForQuery = function () {
// remove instance callback
const cb = this._queryCallback
this._queryCallback = undefined
// remove instance query error
const err = this._queryError
this._queryError = undefined
// remove instance rows
const rows = this._rows
this._rows = undefined
// remove instance results
const results = this._results
this._results = undefined
this._resultCount = 0
if (cb) {
cb(err, rows || [], results)
}
}