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
4 changes: 4 additions & 0 deletions docs/pages/features/queries.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Custom conversion to a UTC date string.

Buffer instances are unchanged.

**ArrayBuffer and ArrayBuffer views**

Converted to a Buffer. An ArrayBuffer includes all of its bytes; typed arrays and DataView values include only the bytes within the view.

**Array**

Converted to a string that describes a Postgres array. Each array item is recursively converted using the rules described here.
Expand Down
7 changes: 6 additions & 1 deletion packages/pg/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const defaults = require('./defaults')
const nodeUtils = require('util')

const { isDate } = require('util/types')
const { isDate, isArrayBuffer } = require('util/types')

const invalidDateDeprecationNotice = nodeUtils.deprecate(
() => {},
Expand Down Expand Up @@ -36,6 +36,8 @@ function arrayString(val) {
item = Buffer.from(item.buffer, item.byteOffset, item.byteLength)
}
result += '\\\\x' + item.toString('hex')
} else if (isArrayBuffer(item)) {
result += '\\\\x' + Buffer.from(item).toString('hex')
} else {
result += escapeElement(prepareValue(item))
}
Expand All @@ -60,6 +62,9 @@ const prepareValue = function (val, seen) {
if (ArrayBuffer.isView(val)) {
return Buffer.from(val.buffer, val.byteOffset, val.byteLength)
}
if (isArrayBuffer(val)) {
return Buffer.from(val)
}
if (isDate(val)) {
if (isNaN(val.getTime())) {
invalidDateDeprecationNotice()
Expand Down
85 changes: 85 additions & 0 deletions packages/pg/test/integration/client/arraybuffer-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'
const helper = require('./test-helper')
const assert = require('assert')
const vm = require('vm')
const suite = new helper.Suite()

suite.test('ArrayBuffer parameters work with cached prepared queries', async function () {
const client = new helper.pg.Client(helper.config)
await client.connect()
try {
for (const bytes of [[], [65, 66, 67], [68, 69]]) {
const value = Uint8Array.from(bytes).buffer
const result = await client.query({
name: 'arraybuffer-parameter',
text: 'SELECT $1::bytea AS value',
values: [value],
})
assert.deepStrictEqual(result.rows[0].value, Buffer.from(bytes))
}
} finally {
await client.end()
}
})

// libpq currently accepts scalar Buffer parameters as text, so arbitrary binary
// scalar parameters are only supported by the JavaScript client.
if (!helper.args.native) {
suite.test('ArrayBuffer round-trips packed metadata and a floating-point payload', async function () {
const client = new helper.pg.Client(helper.config)
await client.connect()
try {
const value = new ArrayBuffer(12)
new Uint8Array(value, 0, 4).set([0, 128, 255, 42])
new Float32Array(value, 4).set([1.5, -2.5])
await client.query('CREATE TEMP TABLE packed_data (value bytea)')
await client.query('INSERT INTO packed_data VALUES ($1)', [value])
const result = await client.query('SELECT value FROM packed_data')
assert.deepStrictEqual(Array.from(result.rows[0].value), Array.from(new Uint8Array(value)))
} finally {
await client.end()
}
})

suite.test('ArrayBuffer parameters preserve all bytes, cross-realm values and view bounds', async function () {
const client = new helper.pg.Client(helper.config)
await client.connect()
try {
const bytes = Uint8Array.from({ length: 256 }, (_, i) => i)
const foreign = vm.runInNewContext('Uint8Array.from([0, 128, 255]).buffer')
const cases = [
[bytes.buffer, Buffer.from(bytes)],
[foreign, Buffer.from([0, 128, 255])],
[new DataView(bytes.buffer, 127, 3), Buffer.from([127, 128, 129])],
[bytes.subarray(127, 130), Buffer.from([127, 128, 129])],
[{ toPostgres: () => foreign }, Buffer.from([0, 128, 255])],
]
for (const [value, expected] of cases) {
const result = await client.query('SELECT $1::bytea AS value', [value])
assert.deepStrictEqual(result.rows[0].value, expected)
}
} finally {
await client.end()
}
})
}

suite.test('ArrayBuffer arrays round-trip binary values and nulls', async function () {
const client = new helper.pg.Client(helper.config)
await client.connect()
try {
const bytes = Uint8Array.from({ length: 256 }, (_, i) => i)
const foreign = vm.runInNewContext('Uint8Array.from([0, 128, 255]).buffer')
const value = [
[bytes.buffer, new ArrayBuffer(0), null],
[foreign, bytes.subarray(127, 130), new DataView(bytes.buffer, 254, 2)],
]
const result = await client.query('SELECT $1::bytea[] AS value', [value])
assert.deepStrictEqual(result.rows[0].value, [
[Buffer.from(bytes), Buffer.alloc(0), null],
[Buffer.from([0, 128, 255]), Buffer.from([127, 128, 129]), Buffer.from([254, 255])],
])
} finally {
await client.end()
}
})
70 changes: 70 additions & 0 deletions packages/pg/test/unit/arraybuffer-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict'
const helper = require('./test-helper')
const utils = require('../../lib/utils')
const assert = require('assert')
const vm = require('vm')
const suite = new helper.Suite()
const test = suite.test.bind(suite)

test('prepareValue: ArrayBuffer includes all packed views', function () {
const value = new ArrayBuffer(12)
new Uint8Array(value, 0, 4).set([0, 128, 255, 42])
new Float32Array(value, 4).set([1.5, -2.5])

const out = utils.prepareValue(value)
assert.ok(Buffer.isBuffer(out))
assert.deepStrictEqual(Array.from(out), Array.from(new Uint8Array(value)))
})

test('prepareValue: empty ArrayBuffer is an empty Buffer', function () {
assert.deepStrictEqual(utils.prepareValue(new ArrayBuffer(0)), Buffer.alloc(0))
})

test('prepareValue: ArrayBuffer preserves every byte value', function () {
const bytes = Uint8Array.from({ length: 256 }, (_, i) => i)
assert.deepStrictEqual(utils.prepareValue(bytes.buffer), Buffer.from(bytes))
})

test('prepareValue: ArrayBuffer from another realm is binary', function () {
const value = vm.runInNewContext('Uint8Array.from([0, 128, 255]).buffer')
assert.ok(!(value instanceof ArrayBuffer))
assert.deepStrictEqual(utils.prepareValue(value), Buffer.from([0, 128, 255]))
})

test('prepareValue: custom scalar conversion can return an ArrayBuffer', function () {
const value = {
toPostgres: function () {
return Uint8Array.from([0, 128, 255]).buffer
},
}
assert.deepStrictEqual(utils.prepareValue(value), Buffer.from([0, 128, 255]))
})

test('prepareValue: arrays mix ArrayBuffers and bounded views', function () {
const bytes = Uint8Array.from([0, 128, 255, 42])
const values = [bytes.buffer, bytes.subarray(1, 3), new DataView(bytes.buffer, 2, 1), new ArrayBuffer(0)]
assert.strictEqual(utils.prepareValue(values), '{\\\\x0080ff2a,\\\\x80ff,\\\\xff,\\\\x}')
})

test('prepareValue: nested ArrayBuffer arrays preserve nulls', function () {
const value = vm.runInNewContext('Uint8Array.from([0, 128, 255]).buffer')
assert.strictEqual(
utils.prepareValue([
[value, null],
[new ArrayBuffer(0), undefined],
]),
'{{\\\\x0080ff,NULL},{\\\\x,NULL}}'
)
})

test('prepareValue: DataView and typed array slices keep their byte boundaries', function () {
const bytes = Uint8Array.from([0, 1, 128, 255, 42, 43])
assert.deepStrictEqual(utils.prepareValue(new DataView(bytes.buffer, 2, 2)), Buffer.from([128, 255]))
assert.deepStrictEqual(utils.prepareValue(new Uint16Array(bytes.buffer, 2, 1)), Buffer.from([128, 255]))
})

test('prepareValue: ArrayBuffer lookalikes remain JSON objects', function () {
const value = { byteLength: 3, [Symbol.toStringTag]: 'ArrayBuffer' }
assert.strictEqual(utils.prepareValue(value), '{"byteLength":3}')
assert.strictEqual(utils.prepareValue([value]), utils.prepareValue([{ byteLength: 3 }]))
})
Loading