Skip to content

Commit

Permalink
feat(NODE-4870): Support BigInt serialization (#541)
Browse files Browse the repository at this point in the history
  • Loading branch information
W-A-James committed Jan 4, 2023
1 parent 5b837a9 commit e9e40a2
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 103 deletions.
32 changes: 19 additions & 13 deletions src/parser/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@ import type { MinKey } from '../min_key';
import type { ObjectId } from '../objectid';
import type { BSONRegExp } from '../regexp';
import { ByteUtils } from '../utils/byte_utils';
import {
isAnyArrayBuffer,
isBigInt64Array,
isBigUInt64Array,
isDate,
isMap,
isRegExp,
isUint8Array
} from './utils';
import { isAnyArrayBuffer, isDate, isMap, isRegExp, isUint8Array } from './utils';

/** @public */
export interface SerializeOptions {
Expand Down Expand Up @@ -103,6 +95,20 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index:
return index;
}

function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: number) {
buffer[index++] = constants.BSON_DATA_LONG;
// Number of written bytes
const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
// Encode the name
index += numberOfWrittenBytes;
buffer[index++] = 0;
NUMBER_SPACE.setBigInt64(0, value, true);
// Write BigInt value
buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index);
index += EIGHT_BYTE_VIEW_ON_NUMBER.byteLength;
return index;
}

function serializeNull(buffer: Uint8Array, key: string, _: unknown, index: number) {
// Set long type
buffer[index++] = constants.BSON_DATA_NULL;
Expand Down Expand Up @@ -675,7 +681,7 @@ export function serializeInto(
} else if (typeof value === 'number') {
index = serializeNumber(buffer, key, value, index);
} else if (typeof value === 'bigint') {
throw new BSONError('Unsupported type BigInt, please use Decimal128');
index = serializeBigInt(buffer, key, value, index);
} else if (typeof value === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
Expand Down Expand Up @@ -777,8 +783,8 @@ export function serializeInto(
index = serializeString(buffer, key, value, index);
} else if (type === 'number') {
index = serializeNumber(buffer, key, value, index);
} else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) {
throw new BSONError('Unsupported type BigInt, please use Decimal128');
} else if (type === 'bigint') {
index = serializeBigInt(buffer, key, value, index);
} else if (type === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
Expand Down Expand Up @@ -881,7 +887,7 @@ export function serializeInto(
} else if (type === 'number') {
index = serializeNumber(buffer, key, value, index);
} else if (type === 'bigint') {
throw new BSONError('Unsupported type BigInt, please use Decimal128');
index = serializeBigInt(buffer, key, value, index);
} else if (type === 'boolean') {
index = serializeBoolean(buffer, key, value, index);
} else if (value instanceof Date || isDate(value)) {
Expand Down
166 changes: 166 additions & 0 deletions test/node/bigint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { BSON } from '../register-bson';
import { bufferFromHexArray } from './tools/utils';
import { BSON_DATA_LONG } from '../../src/constants';
import { BSONDataView } from '../../src/utils/byte_utils';

describe('BSON BigInt serialization Support', function () {
// Index for the data type byte of a BSON document with a
// NOTE: These offsets only apply for documents with the shape {a : <n>}
// where n is a BigInt
type SerializedDocParts = {
dataType: number;
key: string;
value: bigint;
};
/**
* NOTE: this function operates on serialized BSON documents with the shape { <key> : <n> }
* where n is some int64. This function assumes that keys are properly encoded
* with the necessary null byte at the end and only at the end of the key string
*/
function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts {
const DATA_TYPE_OFFSET = 4;
const KEY_OFFSET = 5;

const dataView = BSONDataView.fromUint8Array(serializedDoc);
const keySlice = serializedDoc.slice(KEY_OFFSET);

let keyLength = 0;
while (keySlice[keyLength++] !== 0);

const valueOffset = KEY_OFFSET + keyLength;
const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString(
'utf8'
);

return {
dataType: dataView.getInt8(DATA_TYPE_OFFSET),
key: key.slice(0, keyLength - 1),
value: dataView.getBigInt64(valueOffset, true)
};
}

it('serializes bigints with the correct BSON type', function () {
const testDoc = { a: 0n };
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG);
});

it('serializes bigints into little-endian byte order', function () {
const testDoc = { a: 0x1234567812345678n };
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
const expectedResult = getSerializedDocParts(
bufferFromHexArray([
'12', // int64 type
'6100', // 'a' key with null terminator
'7856341278563412'
])
);

expect(expectedResult.value).to.equal(serializedDoc.value);
});

it('serializes a BigInt that can be safely represented as a Number', function () {
const testDoc = { a: 0x23n };
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
const expectedResult = getSerializedDocParts(
bufferFromHexArray([
'12', // int64 type
'6100', // 'a' key with null terminator
'2300000000000000' // little endian int64
])
);
expect(serializedDoc).to.deep.equal(expectedResult);
});

it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () {
const testDoc = { a: 0xfffffffffffffff1n };
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
const expectedResult = getSerializedDocParts(
bufferFromHexArray([
'12', // int64
'6100', // 'a' key with null terminator
'f1ffffffffffffff'
])
);
expect(serializedDoc).to.deep.equal(expectedResult);
});

it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () {
const maxIntPlusOne = { a: 2n ** 63n };
const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne));
const expectedResultForMaxIntPlusOne = getSerializedDocParts(
bufferFromHexArray([
'12', // int64
'6100', // 'a' key with null terminator
'0000000000000080'
])
);
expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne);
});

it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () {
const maxPositiveInt64 = { a: 2n ** 63n - 1n };
const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64));
const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts(
bufferFromHexArray([
'12', // int64
'6100', // 'a' key with null terminator
'ffffffffffffff7f'
])
);
expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64);

const minPositiveInt64 = { a: -(2n ** 63n) };
const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64));
const expectedSerializationForMinPositiveInt64 = getSerializedDocParts(
bufferFromHexArray([
'12', // int64
'6100', // 'a' key with null terminator
'0000000000000080'
])
);
expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64);
});

it('truncates a BigInt that is larger than a 64-bit int', function () {
const testDoc = { a: 2n ** 64n + 1n };
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
const expectedSerialization = getSerializedDocParts(
bufferFromHexArray([
'12', //int64
'6100', // 'a' key with null terminator
'0100000000000000'
])
);
expect(serializedDoc).to.deep.equal(expectedSerialization);
});

it('serializes array of BigInts', function () {
const testArr = { a: [1n] };
const serializedArr = BSON.serialize(testArr);
const expectedSerialization = bufferFromHexArray([
'04', // array
'6100', // 'a' key with null terminator
bufferFromHexArray([
'12', // int64
'3000', // '0' key with null terminator
'0100000000000000' // 1n (little-endian)
]).toString('hex')
]);
expect(serializedArr).to.deep.equal(expectedSerialization);
});

it('serializes Map with BigInt values', function () {
const testMap = new Map();
testMap.set('a', 1n);
const serializedMap = getSerializedDocParts(BSON.serialize(testMap));
const expectedSerialization = getSerializedDocParts(
bufferFromHexArray([
'12', // int64
'6100', // 'a' key with null terminator
'0100000000000000'
])
);
expect(serializedMap).to.deep.equal(expectedSerialization);
});
});
72 changes: 0 additions & 72 deletions test/node/bigint_tests.js

This file was deleted.

24 changes: 24 additions & 0 deletions test/node/long.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Long } from '../register-bson';

describe('Long', function () {
it('accepts strings in the constructor', function () {
expect(new Long('0').toString()).to.equal('0');
expect(new Long('00').toString()).to.equal('0');
expect(new Long('-1').toString()).to.equal('-1');
expect(new Long('-1', true).toString()).to.equal('18446744073709551615');
expect(new Long('123456789123456789').toString()).to.equal('123456789123456789');
expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789');
expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904');
expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712');
});

it('accepts BigInts in Long constructor', function () {
expect(new Long(0n).toString()).to.equal('0');
expect(new Long(-1n).toString()).to.equal('-1');
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
});
});
18 changes: 0 additions & 18 deletions test/node/long_tests.js

This file was deleted.

0 comments on commit e9e40a2

Please sign in to comment.