mirror of
https://github.com/brianc/node-postgres.git
synced 2026-01-18 15:55:05 +00:00
Move from 3 loops (prepareValue, check for buffers, write param types, write param values) to a single loop. This speeds up the insert benchmark by around 100 queries per second. Performance improvement depends on number of parameters being bound.
275 lines
7.0 KiB
TypeScript
275 lines
7.0 KiB
TypeScript
import { Writer } from './buffer-writer'
|
|
|
|
const enum code {
|
|
startup = 0x70,
|
|
query = 0x51,
|
|
parse = 0x50,
|
|
bind = 0x42,
|
|
execute = 0x45,
|
|
flush = 0x48,
|
|
sync = 0x53,
|
|
end = 0x58,
|
|
close = 0x43,
|
|
describe = 0x44,
|
|
copyFromChunk = 0x64,
|
|
copyDone = 0x63,
|
|
copyFail = 0x66,
|
|
}
|
|
|
|
const writer = new Writer()
|
|
|
|
const startup = (opts: Record<string, string>): Buffer => {
|
|
// protocol version
|
|
writer.addInt16(3).addInt16(0)
|
|
for (const key of Object.keys(opts)) {
|
|
writer.addCString(key).addCString(opts[key])
|
|
}
|
|
|
|
writer.addCString('client_encoding').addCString('UTF8')
|
|
|
|
var bodyBuffer = writer.addCString('').flush()
|
|
// this message is sent without a code
|
|
|
|
var length = bodyBuffer.length + 4
|
|
|
|
return new Writer().addInt32(length).add(bodyBuffer).flush()
|
|
}
|
|
|
|
const requestSsl = (): Buffer => {
|
|
const response = Buffer.allocUnsafe(8)
|
|
response.writeInt32BE(8, 0)
|
|
response.writeInt32BE(80877103, 4)
|
|
return response
|
|
}
|
|
|
|
const password = (password: string): Buffer => {
|
|
return writer.addCString(password).flush(code.startup)
|
|
}
|
|
|
|
const sendSASLInitialResponseMessage = function (mechanism: string, initialResponse: string): Buffer {
|
|
// 0x70 = 'p'
|
|
writer.addCString(mechanism).addInt32(Buffer.byteLength(initialResponse)).addString(initialResponse)
|
|
|
|
return writer.flush(code.startup)
|
|
}
|
|
|
|
const sendSCRAMClientFinalMessage = function (additionalData: string): Buffer {
|
|
return writer.addString(additionalData).flush(code.startup)
|
|
}
|
|
|
|
const query = (text: string): Buffer => {
|
|
return writer.addCString(text).flush(code.query)
|
|
}
|
|
|
|
type ParseOpts = {
|
|
name?: string
|
|
types?: number[]
|
|
text: string
|
|
}
|
|
|
|
const emptyArray: any[] = []
|
|
|
|
const parse = (query: ParseOpts): Buffer => {
|
|
// expect something like this:
|
|
// { name: 'queryName',
|
|
// text: 'select * from blah',
|
|
// types: ['int8', 'bool'] }
|
|
|
|
// normalize missing query names to allow for null
|
|
const name = query.name || ''
|
|
if (name.length > 63) {
|
|
/* eslint-disable no-console */
|
|
console.error('Warning! Postgres only supports 63 characters for query names.')
|
|
console.error('You supplied %s (%s)', name, name.length)
|
|
console.error('This can cause conflicts and silent errors executing queries')
|
|
/* eslint-enable no-console */
|
|
}
|
|
|
|
const types = query.types || emptyArray
|
|
|
|
var len = types.length
|
|
|
|
var buffer = writer
|
|
.addCString(name) // name of query
|
|
.addCString(query.text) // actual query text
|
|
.addInt16(len)
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
buffer.addInt32(types[i])
|
|
}
|
|
|
|
return writer.flush(code.parse)
|
|
}
|
|
|
|
type ValueMapper = (param: any, index: number) => any
|
|
|
|
type BindOpts = {
|
|
portal?: string
|
|
binary?: boolean
|
|
statement?: string
|
|
values?: any[]
|
|
// optional map from JS value to postgres value per parameter
|
|
valueMapper?: ValueMapper
|
|
}
|
|
|
|
const paramWriter = new Writer()
|
|
|
|
// make this a const enum so typescript will inline the value
|
|
const enum ParamType {
|
|
STRING = 0,
|
|
BINARY = 1,
|
|
}
|
|
|
|
const writeValues = function (values: any[], valueMapper?: ValueMapper): void {
|
|
for (let i = 0; i < values.length; i++) {
|
|
const mappedVal = valueMapper ? valueMapper(values[i], i) : values[i]
|
|
if (mappedVal == null) {
|
|
// add the param type (string) to the writer
|
|
writer.addInt16(ParamType.STRING)
|
|
// write -1 to the param writer to indicate null
|
|
paramWriter.addInt32(-1)
|
|
} else if (mappedVal instanceof Buffer) {
|
|
// add the param type (binary) to the writer
|
|
writer.addInt16(ParamType.BINARY)
|
|
// add the buffer to the param writer
|
|
paramWriter.addInt32(mappedVal.length)
|
|
paramWriter.add(mappedVal)
|
|
} else {
|
|
// add the param type (string) to the writer
|
|
writer.addInt16(ParamType.STRING)
|
|
paramWriter.addInt32(Buffer.byteLength(mappedVal))
|
|
paramWriter.addString(mappedVal)
|
|
}
|
|
}
|
|
}
|
|
|
|
const bind = (config: BindOpts = {}): Buffer => {
|
|
// normalize config
|
|
const portal = config.portal || ''
|
|
const statement = config.statement || ''
|
|
const binary = config.binary || false
|
|
const values = config.values || emptyArray
|
|
const len = values.length
|
|
|
|
writer.addCString(portal).addCString(statement)
|
|
writer.addInt16(len)
|
|
|
|
writeValues(values, config.valueMapper)
|
|
|
|
writer.addInt16(len)
|
|
writer.add(paramWriter.flush())
|
|
|
|
// format code
|
|
writer.addInt16(binary ? ParamType.BINARY : ParamType.STRING)
|
|
return writer.flush(code.bind)
|
|
}
|
|
|
|
type ExecOpts = {
|
|
portal?: string
|
|
rows?: number
|
|
}
|
|
|
|
const emptyExecute = Buffer.from([code.execute, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00])
|
|
|
|
const execute = (config?: ExecOpts): Buffer => {
|
|
// this is the happy path for most queries
|
|
if (!config || (!config.portal && !config.rows)) {
|
|
return emptyExecute
|
|
}
|
|
|
|
const portal = config.portal || ''
|
|
const rows = config.rows || 0
|
|
|
|
const portalLength = Buffer.byteLength(portal)
|
|
const len = 4 + portalLength + 1 + 4
|
|
// one extra bit for code
|
|
const buff = Buffer.allocUnsafe(1 + len)
|
|
buff[0] = code.execute
|
|
buff.writeInt32BE(len, 1)
|
|
buff.write(portal, 5, 'utf-8')
|
|
buff[portalLength + 5] = 0 // null terminate portal cString
|
|
buff.writeUInt32BE(rows, buff.length - 4)
|
|
return buff
|
|
}
|
|
|
|
const cancel = (processID: number, secretKey: number): Buffer => {
|
|
const buffer = Buffer.allocUnsafe(16)
|
|
buffer.writeInt32BE(16, 0)
|
|
buffer.writeInt16BE(1234, 4)
|
|
buffer.writeInt16BE(5678, 6)
|
|
buffer.writeInt32BE(processID, 8)
|
|
buffer.writeInt32BE(secretKey, 12)
|
|
return buffer
|
|
}
|
|
|
|
type PortalOpts = {
|
|
type: 'S' | 'P'
|
|
name?: string
|
|
}
|
|
|
|
const cstringMessage = (code: code, string: string): Buffer => {
|
|
const stringLen = Buffer.byteLength(string)
|
|
const len = 4 + stringLen + 1
|
|
// one extra bit for code
|
|
const buffer = Buffer.allocUnsafe(1 + len)
|
|
buffer[0] = code
|
|
buffer.writeInt32BE(len, 1)
|
|
buffer.write(string, 5, 'utf-8')
|
|
buffer[len] = 0 // null terminate cString
|
|
return buffer
|
|
}
|
|
|
|
const emptyDescribePortal = writer.addCString('P').flush(code.describe)
|
|
const emptyDescribeStatement = writer.addCString('S').flush(code.describe)
|
|
|
|
const describe = (msg: PortalOpts): Buffer => {
|
|
return msg.name
|
|
? cstringMessage(code.describe, `${msg.type}${msg.name || ''}`)
|
|
: msg.type === 'P'
|
|
? emptyDescribePortal
|
|
: emptyDescribeStatement
|
|
}
|
|
|
|
const close = (msg: PortalOpts): Buffer => {
|
|
const text = `${msg.type}${msg.name || ''}`
|
|
return cstringMessage(code.close, text)
|
|
}
|
|
|
|
const copyData = (chunk: Buffer): Buffer => {
|
|
return writer.add(chunk).flush(code.copyFromChunk)
|
|
}
|
|
|
|
const copyFail = (message: string): Buffer => {
|
|
return cstringMessage(code.copyFail, message)
|
|
}
|
|
|
|
const codeOnlyBuffer = (code: code): Buffer => Buffer.from([code, 0x00, 0x00, 0x00, 0x04])
|
|
|
|
const flushBuffer = codeOnlyBuffer(code.flush)
|
|
const syncBuffer = codeOnlyBuffer(code.sync)
|
|
const endBuffer = codeOnlyBuffer(code.end)
|
|
const copyDoneBuffer = codeOnlyBuffer(code.copyDone)
|
|
|
|
const serialize = {
|
|
startup,
|
|
password,
|
|
requestSsl,
|
|
sendSASLInitialResponseMessage,
|
|
sendSCRAMClientFinalMessage,
|
|
query,
|
|
parse,
|
|
bind,
|
|
execute,
|
|
describe,
|
|
close,
|
|
flush: () => flushBuffer,
|
|
sync: () => syncBuffer,
|
|
end: () => endBuffer,
|
|
copyData,
|
|
copyDone: () => copyDoneBuffer,
|
|
copyFail,
|
|
cancel,
|
|
}
|
|
|
|
export { serialize }
|