Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/pg-cursor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const EventEmitter = require('events').EventEmitter
const util = require('util')

let nextUniqueID = 1 // concept borrowed from org.postgresql.core.v3.QueryExecutorImpl
const maxParameterCount = 0xffff

class Cursor extends EventEmitter {
constructor(text, values, config) {
Expand Down Expand Up @@ -43,6 +44,12 @@ class Cursor extends EventEmitter {
}

submit(connection) {
if (this.values && this.values.length > maxParameterCount) {
return new RangeError(
`PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${this.values.length} were supplied`
)
}

this.state = 'submitted'
this.connection = connection
this._portal = 'C_' + nextUniqueID++
Expand Down
50 changes: 50 additions & 0 deletions packages/pg-cursor/test/parameter-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict'
const assert = require('assert')
const EventEmitter = require('events')
const Cursor = require('../')

const maxParameterCount = 0xffff
const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${
maxParameterCount + 1
} were supplied`

const createConnection = function () {
const connection = new EventEmitter()
connection.calls = { parse: 0, bind: 0, describe: 0, flush: 0 }
connection.parse = function () {
connection.calls.parse++
}
connection.bind = function () {
connection.calls.bind++
}
connection.describe = function () {
connection.calls.describe++
}
connection.flush = function () {
connection.calls.flush++
}
return connection
}

describe('parameter count limits', function () {
it('allows the maximum parameter count', function () {
const connection = createConnection()
const cursor = new Cursor('select $1', new Array(maxParameterCount).fill(null))

const err = cursor.submit(connection)

assert.equal(err, undefined)
assert.deepStrictEqual(connection.calls, { parse: 1, bind: 1, describe: 1, flush: 1 })
})

it('rejects too many parameters before sending query messages', function () {
const connection = createConnection()
const cursor = new Cursor('select $1', new Array(maxParameterCount + 1).fill(null))

const err = cursor.submit(connection)

assert(err instanceof RangeError)
assert.equal(err.message, errorMessage)
assert.deepStrictEqual(connection.calls, { parse: 0, bind: 0, describe: 0, flush: 0 })
})
})
49 changes: 49 additions & 0 deletions packages/pg-protocol/src/outbound-serializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,55 @@ describe('serializer', () => {
})
})

describe('parameter count limits', function () {
const maxParameterCount = 0xffff
const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${
maxParameterCount + 1
} were supplied`

it('supports the maximum parameter count in a bind message', function () {
const actual = serialize.bind({ values: new Array(maxParameterCount).fill(null) })
const valueCountOffset = 9 + maxParameterCount * 2

assert.equal(actual.readUInt16BE(7), maxParameterCount)
assert.equal(actual.readUInt16BE(valueCountOffset), maxParameterCount)
})

it('rejects too many bind parameters before mapping values', function () {
let mapped = false

assert.throws(
() =>
serialize.bind({
values: new Array(maxParameterCount + 1),
valueMapper: () => {
mapped = true
return null
},
}),
{ name: 'RangeError', message: errorMessage }
)
assert.equal(mapped, false)

const actual = serialize.query('select 1')
const expected = new BufferList().addCString('select 1').join(true, 'Q')
assert.deepEqual(actual, expected)
})

it('supports the maximum parameter type count in a parse message', function () {
const actual = serialize.parse({ text: '', types: new Array(maxParameterCount).fill(0) })

assert.equal(actual.readUInt16BE(7), maxParameterCount)
})

it('rejects too many parameter types in a parse message', function () {
assert.throws(() => serialize.parse({ text: '', types: new Array(maxParameterCount + 1) }), {
name: 'RangeError',
message: errorMessage,
})
})
})

it('with custom valueMapper', function () {
const actual = serialize.bind({
portal: 'bang',
Expand Down
9 changes: 9 additions & 0 deletions packages/pg-protocol/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const enum code {
}

const writer = new Writer()
const maxParameterCount = 0xffff

const checkParameterCount = (count: number): void => {
if (count > maxParameterCount) {
throw new RangeError(`PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${count} were supplied`)
}
}

const startup = (opts: Record<string, string>): Buffer => {
// protocol version
Expand Down Expand Up @@ -86,6 +93,7 @@ const parse = (query: ParseOpts): Buffer => {
const types = query.types || emptyArray

const len = types.length
checkParameterCount(len)

const buffer = writer
.addCString(name) // name of query
Expand Down Expand Up @@ -150,6 +158,7 @@ const bind = (config: BindOpts = {}): Buffer => {
const binary = config.binary || false
const values = config.values || emptyArray
const len = values.length
checkParameterCount(len)

writer.addCString(portal).addCString(statement)

Expand Down
7 changes: 7 additions & 0 deletions packages/pg/lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const { EventEmitter } = require('events')
const Result = require('./result')
const utils = require('./utils')

const maxParameterCount = 0xffff

class Query extends EventEmitter {
constructor(config, values, callback) {
super()
Expand Down Expand Up @@ -161,6 +163,11 @@ class Query extends EventEmitter {
return new Error('Query values must be an array')
}
if (this.requiresPreparation()) {
if (this.values && this.values.length > maxParameterCount) {
return new RangeError(
`PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${this.values.length} were supplied`
)
}
// If we're using the extended query protocol we fire off several separate commands
// to the backend. On some versions of node & some operating system versions
// the network stack writes each message separately instead of buffering them together
Expand Down
55 changes: 55 additions & 0 deletions packages/pg/test/integration/gh-issues/2579-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict'
const assert = require('assert')
const helper = require('../test-helper')

const suite = new helper.Suite()
const maxParameterCount = 0xffff
const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${
maxParameterCount + 1
} were supplied`

suite.test(
'rejects too many parameters and keeps the connection usable',
!helper.args.native &&
async function () {
const client = new helper.Client()
await client.connect()
try {
await assert.rejects(client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }), {
name: 'RangeError',
message: errorMessage,
})

const result = await client.query('select 1 as value')
assert.equal(result.rows[0].value, 1)
} finally {
await client.end()
}
}
)

suite.test(
'isolates an oversized query in pipeline mode',
!helper.args.native &&
async function () {
const client = new helper.Client({ pipeline: true })
await client.connect()
try {
const results = await Promise.allSettled([
client.query('select 1 as value'),
client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }),
client.query('select 2 as value'),
])

assert.equal(results[0].status, 'fulfilled')
assert.equal(results[0].value.rows[0].value, 1)
assert.equal(results[1].status, 'rejected')
assert(results[1].reason instanceof RangeError)
assert.equal(results[1].reason.message, errorMessage)
assert.equal(results[2].status, 'fulfilled')
assert.equal(results[2].value.rows[0].value, 2)
} finally {
await client.end()
}
}
)
86 changes: 86 additions & 0 deletions packages/pg/test/unit/client/parameter-count-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict'
const assert = require('assert')
const helper = require('./test-helper')
const Query = require('../../../lib/query')

const suite = new helper.Suite()
const maxParameterCount = 0xffff
const errorMessage = `PostgreSQL supports a maximum of ${maxParameterCount} parameters, but ${
maxParameterCount + 1
} were supplied`

suite.test('allows the maximum parameter count', function () {
const client = helper.client()
const query = new Query({
text: 'select $1',
values: new Array(maxParameterCount).fill(null),
})

const err = query.submit(client.connection)

assert.equal(err, null)
assert.equal(client.connection.parseMessages.length, 1)
assert.equal(client.connection.bindMessages.length, 1)
assert.equal(client.connection.syncCount, 1)
})

suite.test('rejects too many parameters before sending query messages', function (done) {
const client = helper.client()
const con = client.connection
let callbackCalls = 0
let queryReturned = false

con.emit('readyForQuery')
client.query(
{
text: 'select $1',
values: new Array(maxParameterCount + 1),
},
function (err) {
callbackCalls++
assert.equal(queryReturned, true, 'the callback must be asynchronous')
assert(err instanceof RangeError)
assert.equal(err.message, errorMessage)
assert.equal(con.parseMessages.length, 0)
assert.equal(con.bindMessages.length, 0)
assert.equal(con.syncCount, 0)

client.query('select 1', function (err) {
assert.ifError(err)
assert.equal(callbackCalls, 1)
done()
})
process.nextTick(function () {
con.emit('readyForQuery')
})
}
)
queryReturned = true
})

suite.test('isolates an oversized query in pipeline mode', async function () {
const client = helper.client({ pipeline: true })
const con = client.connection
con.emit('readyForQuery')

const queries = [
client.query('select 1'),
client.query({ text: 'select $1', values: new Array(maxParameterCount + 1) }),
client.query('select 2'),
]

assert.deepStrictEqual(con.queries, ['select 1', 'select 2'])
assert.equal(con.parseMessages.length, 0)
assert.equal(con.bindMessages.length, 0)
assert.equal(con.syncCount, 0)

con.emit('readyForQuery')
con.emit('readyForQuery')

const results = await Promise.allSettled(queries)
assert.equal(results[0].status, 'fulfilled')
assert.equal(results[1].status, 'rejected')
assert(results[1].reason instanceof RangeError)
assert.equal(results[1].reason.message, errorMessage)
assert.equal(results[2].status, 'fulfilled')
})
Loading