Skip to content

Commit e9e40a2

Browse files
authored
feat(NODE-4870): Support BigInt serialization (#541)
1 parent 5b837a9 commit e9e40a2

File tree

5 files changed

+209
-103
lines changed

5 files changed

+209
-103
lines changed

src/parser/serializer.ts

+19-13
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,7 @@ import type { MinKey } from '../min_key';
1212
import type { ObjectId } from '../objectid';
1313
import type { BSONRegExp } from '../regexp';
1414
import { ByteUtils } from '../utils/byte_utils';
15-
import {
16-
isAnyArrayBuffer,
17-
isBigInt64Array,
18-
isBigUInt64Array,
19-
isDate,
20-
isMap,
21-
isRegExp,
22-
isUint8Array
23-
} from './utils';
15+
import { isAnyArrayBuffer, isDate, isMap, isRegExp, isUint8Array } from './utils';
2416

2517
/** @public */
2618
export interface SerializeOptions {
@@ -103,6 +95,20 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index:
10395
return index;
10496
}
10597

98+
function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: number) {
99+
buffer[index++] = constants.BSON_DATA_LONG;
100+
// Number of written bytes
101+
const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index);
102+
// Encode the name
103+
index += numberOfWrittenBytes;
104+
buffer[index++] = 0;
105+
NUMBER_SPACE.setBigInt64(0, value, true);
106+
// Write BigInt value
107+
buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index);
108+
index += EIGHT_BYTE_VIEW_ON_NUMBER.byteLength;
109+
return index;
110+
}
111+
106112
function serializeNull(buffer: Uint8Array, key: string, _: unknown, index: number) {
107113
// Set long type
108114
buffer[index++] = constants.BSON_DATA_NULL;
@@ -675,7 +681,7 @@ export function serializeInto(
675681
} else if (typeof value === 'number') {
676682
index = serializeNumber(buffer, key, value, index);
677683
} else if (typeof value === 'bigint') {
678-
throw new BSONError('Unsupported type BigInt, please use Decimal128');
684+
index = serializeBigInt(buffer, key, value, index);
679685
} else if (typeof value === 'boolean') {
680686
index = serializeBoolean(buffer, key, value, index);
681687
} else if (value instanceof Date || isDate(value)) {
@@ -777,8 +783,8 @@ export function serializeInto(
777783
index = serializeString(buffer, key, value, index);
778784
} else if (type === 'number') {
779785
index = serializeNumber(buffer, key, value, index);
780-
} else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) {
781-
throw new BSONError('Unsupported type BigInt, please use Decimal128');
786+
} else if (type === 'bigint') {
787+
index = serializeBigInt(buffer, key, value, index);
782788
} else if (type === 'boolean') {
783789
index = serializeBoolean(buffer, key, value, index);
784790
} else if (value instanceof Date || isDate(value)) {
@@ -881,7 +887,7 @@ export function serializeInto(
881887
} else if (type === 'number') {
882888
index = serializeNumber(buffer, key, value, index);
883889
} else if (type === 'bigint') {
884-
throw new BSONError('Unsupported type BigInt, please use Decimal128');
890+
index = serializeBigInt(buffer, key, value, index);
885891
} else if (type === 'boolean') {
886892
index = serializeBoolean(buffer, key, value, index);
887893
} else if (value instanceof Date || isDate(value)) {

test/node/bigint.test.ts

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { BSON } from '../register-bson';
2+
import { bufferFromHexArray } from './tools/utils';
3+
import { BSON_DATA_LONG } from '../../src/constants';
4+
import { BSONDataView } from '../../src/utils/byte_utils';
5+
6+
describe('BSON BigInt serialization Support', function () {
7+
// Index for the data type byte of a BSON document with a
8+
// NOTE: These offsets only apply for documents with the shape {a : <n>}
9+
// where n is a BigInt
10+
type SerializedDocParts = {
11+
dataType: number;
12+
key: string;
13+
value: bigint;
14+
};
15+
/**
16+
* NOTE: this function operates on serialized BSON documents with the shape { <key> : <n> }
17+
* where n is some int64. This function assumes that keys are properly encoded
18+
* with the necessary null byte at the end and only at the end of the key string
19+
*/
20+
function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts {
21+
const DATA_TYPE_OFFSET = 4;
22+
const KEY_OFFSET = 5;
23+
24+
const dataView = BSONDataView.fromUint8Array(serializedDoc);
25+
const keySlice = serializedDoc.slice(KEY_OFFSET);
26+
27+
let keyLength = 0;
28+
while (keySlice[keyLength++] !== 0);
29+
30+
const valueOffset = KEY_OFFSET + keyLength;
31+
const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString(
32+
'utf8'
33+
);
34+
35+
return {
36+
dataType: dataView.getInt8(DATA_TYPE_OFFSET),
37+
key: key.slice(0, keyLength - 1),
38+
value: dataView.getBigInt64(valueOffset, true)
39+
};
40+
}
41+
42+
it('serializes bigints with the correct BSON type', function () {
43+
const testDoc = { a: 0n };
44+
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
45+
expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG);
46+
});
47+
48+
it('serializes bigints into little-endian byte order', function () {
49+
const testDoc = { a: 0x1234567812345678n };
50+
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
51+
const expectedResult = getSerializedDocParts(
52+
bufferFromHexArray([
53+
'12', // int64 type
54+
'6100', // 'a' key with null terminator
55+
'7856341278563412'
56+
])
57+
);
58+
59+
expect(expectedResult.value).to.equal(serializedDoc.value);
60+
});
61+
62+
it('serializes a BigInt that can be safely represented as a Number', function () {
63+
const testDoc = { a: 0x23n };
64+
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
65+
const expectedResult = getSerializedDocParts(
66+
bufferFromHexArray([
67+
'12', // int64 type
68+
'6100', // 'a' key with null terminator
69+
'2300000000000000' // little endian int64
70+
])
71+
);
72+
expect(serializedDoc).to.deep.equal(expectedResult);
73+
});
74+
75+
it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () {
76+
const testDoc = { a: 0xfffffffffffffff1n };
77+
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
78+
const expectedResult = getSerializedDocParts(
79+
bufferFromHexArray([
80+
'12', // int64
81+
'6100', // 'a' key with null terminator
82+
'f1ffffffffffffff'
83+
])
84+
);
85+
expect(serializedDoc).to.deep.equal(expectedResult);
86+
});
87+
88+
it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () {
89+
const maxIntPlusOne = { a: 2n ** 63n };
90+
const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne));
91+
const expectedResultForMaxIntPlusOne = getSerializedDocParts(
92+
bufferFromHexArray([
93+
'12', // int64
94+
'6100', // 'a' key with null terminator
95+
'0000000000000080'
96+
])
97+
);
98+
expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne);
99+
});
100+
101+
it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () {
102+
const maxPositiveInt64 = { a: 2n ** 63n - 1n };
103+
const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64));
104+
const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts(
105+
bufferFromHexArray([
106+
'12', // int64
107+
'6100', // 'a' key with null terminator
108+
'ffffffffffffff7f'
109+
])
110+
);
111+
expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64);
112+
113+
const minPositiveInt64 = { a: -(2n ** 63n) };
114+
const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64));
115+
const expectedSerializationForMinPositiveInt64 = getSerializedDocParts(
116+
bufferFromHexArray([
117+
'12', // int64
118+
'6100', // 'a' key with null terminator
119+
'0000000000000080'
120+
])
121+
);
122+
expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64);
123+
});
124+
125+
it('truncates a BigInt that is larger than a 64-bit int', function () {
126+
const testDoc = { a: 2n ** 64n + 1n };
127+
const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc));
128+
const expectedSerialization = getSerializedDocParts(
129+
bufferFromHexArray([
130+
'12', //int64
131+
'6100', // 'a' key with null terminator
132+
'0100000000000000'
133+
])
134+
);
135+
expect(serializedDoc).to.deep.equal(expectedSerialization);
136+
});
137+
138+
it('serializes array of BigInts', function () {
139+
const testArr = { a: [1n] };
140+
const serializedArr = BSON.serialize(testArr);
141+
const expectedSerialization = bufferFromHexArray([
142+
'04', // array
143+
'6100', // 'a' key with null terminator
144+
bufferFromHexArray([
145+
'12', // int64
146+
'3000', // '0' key with null terminator
147+
'0100000000000000' // 1n (little-endian)
148+
]).toString('hex')
149+
]);
150+
expect(serializedArr).to.deep.equal(expectedSerialization);
151+
});
152+
153+
it('serializes Map with BigInt values', function () {
154+
const testMap = new Map();
155+
testMap.set('a', 1n);
156+
const serializedMap = getSerializedDocParts(BSON.serialize(testMap));
157+
const expectedSerialization = getSerializedDocParts(
158+
bufferFromHexArray([
159+
'12', // int64
160+
'6100', // 'a' key with null terminator
161+
'0100000000000000'
162+
])
163+
);
164+
expect(serializedMap).to.deep.equal(expectedSerialization);
165+
});
166+
});

test/node/bigint_tests.js

-72
This file was deleted.

test/node/long.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Long } from '../register-bson';
2+
3+
describe('Long', function () {
4+
it('accepts strings in the constructor', function () {
5+
expect(new Long('0').toString()).to.equal('0');
6+
expect(new Long('00').toString()).to.equal('0');
7+
expect(new Long('-1').toString()).to.equal('-1');
8+
expect(new Long('-1', true).toString()).to.equal('18446744073709551615');
9+
expect(new Long('123456789123456789').toString()).to.equal('123456789123456789');
10+
expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789');
11+
expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904');
12+
expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712');
13+
});
14+
15+
it('accepts BigInts in Long constructor', function () {
16+
expect(new Long(0n).toString()).to.equal('0');
17+
expect(new Long(-1n).toString()).to.equal('-1');
18+
expect(new Long(-1n, true).toString()).to.equal('18446744073709551615');
19+
expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789');
20+
expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789');
21+
expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904');
22+
expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712');
23+
});
24+
});

test/node/long_tests.js

-18
This file was deleted.

0 commit comments

Comments
 (0)