@powersync/service-module-mongodb 0.15.4 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/replication/ChangeStream.d.ts +6 -6
- package/dist/replication/ChangeStream.js +300 -322
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.js +2 -2
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/JsonBufferWriter.d.ts +80 -0
- package/dist/replication/JsonBufferWriter.js +342 -0
- package/dist/replication/JsonBufferWriter.js.map +1 -0
- package/dist/replication/MongoRelation.js +4 -0
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/MongoSnapshotQuery.d.ts +1 -1
- package/dist/replication/MongoSnapshotQuery.js +6 -3
- package/dist/replication/MongoSnapshotQuery.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +55 -0
- package/dist/replication/RawChangeStream.js +322 -0
- package/dist/replication/RawChangeStream.js.map +1 -0
- package/dist/replication/SourceRowConverter.d.ts +46 -0
- package/dist/replication/SourceRowConverter.js +42 -0
- package/dist/replication/SourceRowConverter.js.map +1 -0
- package/dist/replication/bufferToSqlite.d.ts +43 -0
- package/dist/replication/bufferToSqlite.js +740 -0
- package/dist/replication/bufferToSqlite.js.map +1 -0
- package/dist/replication/internal-mongodb-utils.d.ts +0 -12
- package/dist/replication/internal-mongodb-utils.js +0 -54
- package/dist/replication/internal-mongodb-utils.js.map +1 -1
- package/dist/replication/replication-index.d.ts +2 -0
- package/dist/replication/replication-index.js +2 -0
- package/dist/replication/replication-index.js.map +1 -1
- package/package.json +11 -11
- package/scripts/benchmark-change-document-json.mts +358 -0
- package/scripts/benchmark-change-document.mts +370 -0
- package/src/replication/ChangeStream.ts +348 -371
- package/src/replication/ChangeStreamReplicationJob.ts +2 -2
- package/src/replication/JsonBufferWriter.ts +390 -0
- package/src/replication/MongoRelation.ts +3 -0
- package/src/replication/MongoSnapshotQuery.ts +8 -5
- package/src/replication/RawChangeStream.ts +460 -0
- package/src/replication/SourceRowConverter.ts +65 -0
- package/src/replication/bufferToSqlite.ts +944 -0
- package/src/replication/internal-mongodb-utils.ts +0 -65
- package/src/replication/replication-index.ts +2 -0
- package/test/src/buffer_to_sqlite.test.ts +1146 -0
- package/test/src/change_stream.test.ts +49 -2
- package/test/src/change_stream_utils.ts +4 -10
- package/test/src/mongo_test.test.ts +66 -64
- package/test/src/parse_document_id.test.ts +54 -0
- package/test/src/raw_change_stream.test.ts +547 -0
- package/test/src/resume.test.ts +12 -2
- package/test/src/util.ts +56 -3
- package/test/tsconfig.json +0 -1
- package/tsconfig.scripts.json +13 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/test/src/internal_mongodb_utils.test.ts +0 -103
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { CompatibilityContext, CompatibilityOption, DateTimeValue, TimeValuePrecision } from '@powersync/service-sync-rules';
|
|
3
|
+
import { BYTE_COLON, BYTE_COMMA, BYTE_LBRACE, BYTE_LBRACKET, BYTE_ONE, BYTE_RBRACE, BYTE_RBRACKET, BYTE_SPACE, BYTE_T, BYTE_ZERO, JsonBufferWriter } from './JsonBufferWriter.js';
|
|
4
|
+
const NESTED_DEPTH_LIMIT = 20;
|
|
5
|
+
const SHARED_UTC_DATE = new Date(0);
|
|
6
|
+
const BSON_TYPE_DOUBLE = 0x01;
|
|
7
|
+
const BSON_TYPE_STRING = 0x02;
|
|
8
|
+
const BSON_TYPE_DOCUMENT = 0x03;
|
|
9
|
+
const BSON_TYPE_ARRAY = 0x04;
|
|
10
|
+
const BSON_TYPE_BINARY = 0x05;
|
|
11
|
+
const BSON_TYPE_UNDEFINED = 0x06;
|
|
12
|
+
const BSON_TYPE_OBJECT_ID = 0x07;
|
|
13
|
+
const BSON_TYPE_BOOLEAN = 0x08;
|
|
14
|
+
const BSON_TYPE_UTC_DATETIME = 0x09;
|
|
15
|
+
const BSON_TYPE_NULL = 0x0a;
|
|
16
|
+
const BSON_TYPE_REGEX = 0x0b;
|
|
17
|
+
const BSON_TYPE_DB_POINTER = 0x0c;
|
|
18
|
+
const BSON_TYPE_CODE = 0x0d;
|
|
19
|
+
const BSON_TYPE_SYMBOL = 0x0e;
|
|
20
|
+
const BSON_TYPE_CODE_WITH_SCOPE = 0x0f;
|
|
21
|
+
const BSON_TYPE_INT32 = 0x10;
|
|
22
|
+
const BSON_TYPE_TIMESTAMP = 0x11;
|
|
23
|
+
const BSON_TYPE_INT64 = 0x12;
|
|
24
|
+
const BSON_TYPE_DECIMAL128 = 0x13;
|
|
25
|
+
const BSON_TYPE_MIN_KEY = 0xff;
|
|
26
|
+
const BSON_TYPE_MAX_KEY = 0x7f;
|
|
27
|
+
const BSON_BINARY_SUBTYPE_BYTE_ARRAY = 2;
|
|
28
|
+
const BSON_BINARY_SUBTYPE_UUID = 4;
|
|
29
|
+
// We use a single shared write, to avoid repeatedly re-allocating buffers.
|
|
30
|
+
// Since this is only used in a synchronous call, this is safe.
|
|
31
|
+
// This never releases memory once a large buffer has been allocated, but that is fine
|
|
32
|
+
// for replication use.
|
|
33
|
+
const SHARED_WRITER = new JsonBufferWriter();
|
|
34
|
+
/**
|
|
35
|
+
* Convert a buffer containing BSON bytes to a SqliteRow.
|
|
36
|
+
*
|
|
37
|
+
* This is using a custom BSON parser and JSON serializer for performance reasons. By bypassing bson.deserialize,
|
|
38
|
+
* we avoid many small allocations, and can significantly increase throughput.
|
|
39
|
+
*
|
|
40
|
+
* This attempts to match the behavior of `bson.deserialize -> constructAfterRecord -> applyRowContext` for the most part,
|
|
41
|
+
* with some intentional differences:
|
|
42
|
+
* 1. Regular expression patterns options are preserved as-is, while the above normalizes to JS RegExp values.
|
|
43
|
+
* 2. Full UTF-8 validation is not performed - we attempt to continue using replacement characters, as long as the resulting output remains valid.
|
|
44
|
+
* 3. bson.deserialize has special-case handler for converting documents containing {$ref} -> DBRef. We don't do that here.
|
|
45
|
+
*
|
|
46
|
+
* General principles followed:
|
|
47
|
+
* 1. Correctness: Never produce invalid JSON.
|
|
48
|
+
* 2. Performance: Optimize to be as performant as possible for common cases.
|
|
49
|
+
* 3. Full BSON support: Support all valid BSON documents as input, including deprecated types, but without specifically optimizing for performance here.
|
|
50
|
+
* 4. The source database is responsible for producing valid BSON - we don't test for all edge cases of invalid BSON.
|
|
51
|
+
* 5. We do a best-effort attempt to support "degenerate" BSON cases as documented at https://specifications.readthedocs.io/en/latest/bson-corpus/bson-corpus/, since MongoDB can produce many of these cases.
|
|
52
|
+
*
|
|
53
|
+
* @param bytes the source BSON bytes
|
|
54
|
+
* @param dateRenderMode derive using getDateRenderMode(compatibilityContext)
|
|
55
|
+
*
|
|
56
|
+
* @returns a SqliteRow
|
|
57
|
+
*/
|
|
58
|
+
export function bufferToSqlite(bytes, dateRenderMode) {
|
|
59
|
+
const row = {};
|
|
60
|
+
const jsonWriter = SHARED_WRITER;
|
|
61
|
+
// BSON documents are length-prefixed and null-terminated. We parse directly
|
|
62
|
+
// from raw bytes, so structural validation happens here rather than in the
|
|
63
|
+
// upstream BSON decoder.
|
|
64
|
+
const bodyEnd = readDocumentLength(bytes, 0) - 1;
|
|
65
|
+
let offset = 4;
|
|
66
|
+
while (offset < bodyEnd) {
|
|
67
|
+
const previousOffset = offset;
|
|
68
|
+
const type = bytes[offset++];
|
|
69
|
+
const { value: key, nextOffset: afterKey } = readCString(bytes, offset);
|
|
70
|
+
offset = afterKey;
|
|
71
|
+
switch (type) {
|
|
72
|
+
case BSON_TYPE_OBJECT_ID: {
|
|
73
|
+
row[key] = hexLower(bytes, offset, 12);
|
|
74
|
+
offset += 12;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case BSON_TYPE_STRING: {
|
|
78
|
+
const { value, nextOffset } = readBsonString(bytes, offset);
|
|
79
|
+
row[key] = value;
|
|
80
|
+
offset = nextOffset;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case BSON_TYPE_ARRAY: {
|
|
84
|
+
jsonWriter.reset();
|
|
85
|
+
const result = serializeNestedArrayToJson(bytes, offset, 0, jsonWriter, dateRenderMode);
|
|
86
|
+
row[key] = jsonWriter.toString();
|
|
87
|
+
offset = result.nextOffset;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case BSON_TYPE_DOCUMENT: {
|
|
91
|
+
jsonWriter.reset();
|
|
92
|
+
const result = serializeNestedObjectToJson(bytes, offset, 0, jsonWriter, dateRenderMode);
|
|
93
|
+
row[key] = jsonWriter.toString();
|
|
94
|
+
offset = result.nextOffset;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case BSON_TYPE_BOOLEAN: {
|
|
98
|
+
row[key] = bytes[offset++] ? 1n : 0n;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case BSON_TYPE_UTC_DATETIME: {
|
|
102
|
+
// Even though this is not JSON, we use the same JSON writer for this.
|
|
103
|
+
jsonWriter.reset();
|
|
104
|
+
appendDateTimeToWriter(jsonWriter, Number(bytes.readBigInt64LE(offset)), false, dateRenderMode);
|
|
105
|
+
row[key] = jsonWriter.toString();
|
|
106
|
+
offset += 8;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case BSON_TYPE_INT32: {
|
|
110
|
+
row[key] = BigInt(readInt32LE(bytes, offset));
|
|
111
|
+
offset += 4;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case BSON_TYPE_TIMESTAMP: {
|
|
115
|
+
row[key] = timestampToBigInt(bytes, offset);
|
|
116
|
+
offset += 8;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case BSON_TYPE_INT64: {
|
|
120
|
+
row[key] = bytes.readBigInt64LE(offset);
|
|
121
|
+
offset += 8;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
case BSON_TYPE_DECIMAL128: {
|
|
125
|
+
row[key] = decimal128ToString(bytes, offset);
|
|
126
|
+
offset += 16;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case BSON_TYPE_BINARY: {
|
|
130
|
+
const { value, nextOffset } = parseTopLevelBinary(bytes, offset);
|
|
131
|
+
row[key] = value;
|
|
132
|
+
offset = nextOffset;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case BSON_TYPE_REGEX: {
|
|
136
|
+
const { pattern, options, nextOffset } = parseRegex(bytes, offset);
|
|
137
|
+
jsonWriter.reset();
|
|
138
|
+
writeRegexJson(jsonWriter, pattern, options);
|
|
139
|
+
row[key] = jsonWriter.toString();
|
|
140
|
+
offset = nextOffset;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case BSON_TYPE_DB_POINTER: {
|
|
144
|
+
// DBPointer
|
|
145
|
+
jsonWriter.reset();
|
|
146
|
+
const nextOffset = writeDbPointerJson(bytes, offset, jsonWriter);
|
|
147
|
+
row[key] = jsonWriter.toString();
|
|
148
|
+
offset = nextOffset;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case BSON_TYPE_CODE: {
|
|
152
|
+
// JavaScript code
|
|
153
|
+
jsonWriter.reset();
|
|
154
|
+
const nextOffset = writeCodeJson(bytes, offset, 0, jsonWriter);
|
|
155
|
+
row[key] = jsonWriter.toString();
|
|
156
|
+
offset = nextOffset;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case BSON_TYPE_SYMBOL: {
|
|
160
|
+
// Symbol
|
|
161
|
+
const { value, nextOffset } = readBsonString(bytes, offset);
|
|
162
|
+
row[key] = value;
|
|
163
|
+
offset = nextOffset;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case BSON_TYPE_CODE_WITH_SCOPE: {
|
|
167
|
+
jsonWriter.reset();
|
|
168
|
+
const nextOffset = writeCodeWithScopeJson(bytes, offset, 0, jsonWriter, dateRenderMode);
|
|
169
|
+
row[key] = jsonWriter.toString();
|
|
170
|
+
offset = nextOffset;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case BSON_TYPE_UNDEFINED:
|
|
174
|
+
case BSON_TYPE_NULL:
|
|
175
|
+
case BSON_TYPE_MIN_KEY:
|
|
176
|
+
case BSON_TYPE_MAX_KEY:
|
|
177
|
+
row[key] = null;
|
|
178
|
+
break;
|
|
179
|
+
case BSON_TYPE_DOUBLE: {
|
|
180
|
+
const value = bytes.readDoubleLE(offset);
|
|
181
|
+
offset += 8;
|
|
182
|
+
// Match the default path: integral doubles are widened to bigint.
|
|
183
|
+
row[key] = Number.isInteger(value) ? BigInt(value) : value;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
default: {
|
|
187
|
+
// Unknown top-level types are treated as null for parity with the
|
|
188
|
+
// default converter, but we still advance through the raw bytes.
|
|
189
|
+
row[key] = null;
|
|
190
|
+
offset = skipBsonValue(bytes, offset, type);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
assertAdvanced(previousOffset, offset);
|
|
195
|
+
}
|
|
196
|
+
return row;
|
|
197
|
+
}
|
|
198
|
+
function readInt32LE(bytes, offset) {
|
|
199
|
+
return bytes.readInt32LE(offset);
|
|
200
|
+
}
|
|
201
|
+
function readDocumentLength(bytes, offset) {
|
|
202
|
+
const length = readInt32LE(bytes, offset);
|
|
203
|
+
if (length < 5 || offset + length > bytes.length) {
|
|
204
|
+
throw new Error('Invalid BSON document length');
|
|
205
|
+
}
|
|
206
|
+
if (bytes[offset + length - 1] !== 0) {
|
|
207
|
+
throw new Error('Invalid BSON document terminator');
|
|
208
|
+
}
|
|
209
|
+
return length;
|
|
210
|
+
}
|
|
211
|
+
function readBsonString(bytes, offset) {
|
|
212
|
+
const length = readInt32LE(bytes, offset);
|
|
213
|
+
const stringStart = offset + 4;
|
|
214
|
+
const stringEnd = stringStart + length;
|
|
215
|
+
if (length < 1 || stringEnd > bytes.length) {
|
|
216
|
+
throw new Error('Invalid BSON string length');
|
|
217
|
+
}
|
|
218
|
+
if (bytes[stringEnd - 1] !== 0) {
|
|
219
|
+
throw new Error('Invalid BSON string terminator');
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
value: bytes.toString('utf8', stringStart, stringEnd - 1),
|
|
223
|
+
nextOffset: stringEnd
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function readBsonStringEnd(bytes, offset) {
|
|
227
|
+
const length = readInt32LE(bytes, offset);
|
|
228
|
+
const stringStart = offset + 4;
|
|
229
|
+
const stringEnd = stringStart + length;
|
|
230
|
+
if (length < 1 || stringEnd > bytes.length) {
|
|
231
|
+
throw new Error('Invalid BSON string length');
|
|
232
|
+
}
|
|
233
|
+
if (bytes[stringEnd - 1] !== 0) {
|
|
234
|
+
throw new Error('Invalid BSON string terminator');
|
|
235
|
+
}
|
|
236
|
+
return stringEnd;
|
|
237
|
+
}
|
|
238
|
+
function readCString(bytes, offset) {
|
|
239
|
+
const end = bytes.indexOf(0, offset);
|
|
240
|
+
if (end < 0) {
|
|
241
|
+
throw new Error('Invalid BSON: missing cstring terminator');
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
value: bytes.toString('utf8', offset, end),
|
|
245
|
+
nextOffset: end + 1
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function skipCString(bytes, offset) {
|
|
249
|
+
const end = bytes.indexOf(0, offset);
|
|
250
|
+
if (end < 0) {
|
|
251
|
+
throw new Error('Invalid BSON: missing cstring terminator');
|
|
252
|
+
}
|
|
253
|
+
return end + 1;
|
|
254
|
+
}
|
|
255
|
+
function parseRegex(bytes, offset) {
|
|
256
|
+
const patternEnd = bytes.indexOf(0, offset);
|
|
257
|
+
const optionsEnd = bytes.indexOf(0, patternEnd + 1);
|
|
258
|
+
if (patternEnd < 0 || optionsEnd < 0) {
|
|
259
|
+
throw new Error('Invalid BSON regex');
|
|
260
|
+
}
|
|
261
|
+
const pattern = bytes.toString('utf8', offset, patternEnd);
|
|
262
|
+
return {
|
|
263
|
+
pattern,
|
|
264
|
+
// Preserve the raw BSON option string exactly as encoded. The default path
|
|
265
|
+
// normalizes via JS RegExp semantics, but the custom path intentionally
|
|
266
|
+
// keeps the BSON flags verbatim.
|
|
267
|
+
options: bytes.toString('utf8', patternEnd + 1, optionsEnd),
|
|
268
|
+
nextOffset: optionsEnd + 1
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function decimal128ToString(bytes, offset) {
|
|
272
|
+
// Just use the upstream parser for this
|
|
273
|
+
return new mongo.Decimal128(bytes.subarray(offset, offset + 16)).toString();
|
|
274
|
+
}
|
|
275
|
+
function timestampToBigInt(bytes, offset) {
|
|
276
|
+
return (BigInt(bytes.readUInt32LE(offset + 4)) << 32n) | BigInt(bytes.readUInt32LE(offset));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* @param bytes must be exactly 16 bytes in length - check before calling this.
|
|
280
|
+
* @returns lower-case hex form of the UUID
|
|
281
|
+
*/
|
|
282
|
+
function uuidToString(bytes) {
|
|
283
|
+
const hex = bytes.toString('hex');
|
|
284
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
285
|
+
}
|
|
286
|
+
function parseTopLevelBinary(bytes, offset) {
|
|
287
|
+
const length = readInt32LE(bytes, offset);
|
|
288
|
+
if (length < 0) {
|
|
289
|
+
throw new Error('Invalid BSON binary length');
|
|
290
|
+
}
|
|
291
|
+
const subtype = bytes[offset + 4];
|
|
292
|
+
const dataStart = offset + 5;
|
|
293
|
+
const dataEnd = dataStart + length;
|
|
294
|
+
if (dataEnd > bytes.length) {
|
|
295
|
+
throw new Error('Invalid BSON binary length');
|
|
296
|
+
}
|
|
297
|
+
const data = binaryDataSlice(bytes, dataStart, dataEnd, subtype);
|
|
298
|
+
// Only subtype 4 UUIDs are surfaced as strings. All other binary subtypes
|
|
299
|
+
// stay as raw bytes at the top level.
|
|
300
|
+
if (subtype === BSON_BINARY_SUBTYPE_UUID && data.length === 16) {
|
|
301
|
+
return { value: uuidToString(data), nextOffset: dataEnd };
|
|
302
|
+
}
|
|
303
|
+
return { value: bufferToUint8Array(data), nextOffset: dataEnd };
|
|
304
|
+
}
|
|
305
|
+
function bufferToUint8Array(bytes) {
|
|
306
|
+
return new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Handle a sub-array for binary data, including the legacy 2 subtype.
|
|
310
|
+
*/
|
|
311
|
+
function binaryDataSlice(bytes, dataStart, dataEnd, subtype) {
|
|
312
|
+
if (subtype !== BSON_BINARY_SUBTYPE_BYTE_ARRAY) {
|
|
313
|
+
return bytes.subarray(dataStart, dataEnd);
|
|
314
|
+
}
|
|
315
|
+
// Legacy subtype 2 embeds its own nested length before the actual bytes.
|
|
316
|
+
const legacyLength = readInt32LE(bytes, dataStart);
|
|
317
|
+
const legacyStart = dataStart + 4;
|
|
318
|
+
if (legacyLength < 0 || legacyStart + legacyLength > dataEnd) {
|
|
319
|
+
throw new Error('Invalid BSON legacy binary length');
|
|
320
|
+
}
|
|
321
|
+
return bytes.subarray(legacyStart, legacyStart + legacyLength);
|
|
322
|
+
}
|
|
323
|
+
function skipBsonValue(bytes, offset, type) {
|
|
324
|
+
switch (type) {
|
|
325
|
+
case BSON_TYPE_DOUBLE: // Double
|
|
326
|
+
return offset + 8;
|
|
327
|
+
case BSON_TYPE_STRING: {
|
|
328
|
+
// String
|
|
329
|
+
const length = readInt32LE(bytes, offset);
|
|
330
|
+
return offset + 4 + length;
|
|
331
|
+
}
|
|
332
|
+
case BSON_TYPE_DOCUMENT:
|
|
333
|
+
case BSON_TYPE_ARRAY:
|
|
334
|
+
return offset + readInt32LE(bytes, offset);
|
|
335
|
+
case BSON_TYPE_BINARY: {
|
|
336
|
+
// Binary
|
|
337
|
+
const length = readInt32LE(bytes, offset);
|
|
338
|
+
return offset + 4 + 1 + length;
|
|
339
|
+
}
|
|
340
|
+
case BSON_TYPE_UNDEFINED:
|
|
341
|
+
case BSON_TYPE_NULL:
|
|
342
|
+
case BSON_TYPE_MIN_KEY:
|
|
343
|
+
case BSON_TYPE_MAX_KEY:
|
|
344
|
+
return offset;
|
|
345
|
+
case BSON_TYPE_OBJECT_ID:
|
|
346
|
+
return offset + 12;
|
|
347
|
+
case BSON_TYPE_BOOLEAN:
|
|
348
|
+
return offset + 1;
|
|
349
|
+
case BSON_TYPE_UTC_DATETIME:
|
|
350
|
+
return offset + 8;
|
|
351
|
+
case BSON_TYPE_REGEX: {
|
|
352
|
+
// Regular expression
|
|
353
|
+
const patternEnd = bytes.indexOf(0, offset);
|
|
354
|
+
const optionsEnd = bytes.indexOf(0, patternEnd + 1);
|
|
355
|
+
if (patternEnd < 0 || optionsEnd < 0) {
|
|
356
|
+
throw new Error('Invalid BSON regex');
|
|
357
|
+
}
|
|
358
|
+
return optionsEnd + 1;
|
|
359
|
+
}
|
|
360
|
+
case BSON_TYPE_DB_POINTER: {
|
|
361
|
+
// DBPointer
|
|
362
|
+
const nextOffset = readBsonStringEnd(bytes, offset);
|
|
363
|
+
return nextOffset + 12;
|
|
364
|
+
}
|
|
365
|
+
case BSON_TYPE_CODE: {
|
|
366
|
+
// JavaScript code
|
|
367
|
+
return readBsonStringEnd(bytes, offset);
|
|
368
|
+
}
|
|
369
|
+
case BSON_TYPE_SYMBOL: {
|
|
370
|
+
// Symbol
|
|
371
|
+
return readBsonStringEnd(bytes, offset);
|
|
372
|
+
}
|
|
373
|
+
case BSON_TYPE_CODE_WITH_SCOPE: {
|
|
374
|
+
// JavaScript code with scope
|
|
375
|
+
const length = readInt32LE(bytes, offset);
|
|
376
|
+
return offset + length;
|
|
377
|
+
}
|
|
378
|
+
case BSON_TYPE_INT32:
|
|
379
|
+
return offset + 4;
|
|
380
|
+
case BSON_TYPE_TIMESTAMP:
|
|
381
|
+
return offset + 8;
|
|
382
|
+
case BSON_TYPE_INT64:
|
|
383
|
+
return offset + 8;
|
|
384
|
+
case BSON_TYPE_DECIMAL128:
|
|
385
|
+
return offset + 16;
|
|
386
|
+
default:
|
|
387
|
+
throw new Error(`Unsupported BSON type for skip: 0x${type.toString(16)}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function serializeNestedObjectToJson(bytes, offset, depth, writer, dateRenderMode) {
|
|
391
|
+
if (depth > NESTED_DEPTH_LIMIT) {
|
|
392
|
+
throw new Error(`json nested object depth exceeds the limit of ${NESTED_DEPTH_LIMIT}`);
|
|
393
|
+
}
|
|
394
|
+
const totalLength = readDocumentLength(bytes, offset);
|
|
395
|
+
const bodyEnd = offset + totalLength - 1;
|
|
396
|
+
let cursor = offset + 4;
|
|
397
|
+
writer.writeByte(BYTE_LBRACE);
|
|
398
|
+
let first = true;
|
|
399
|
+
while (cursor < bodyEnd) {
|
|
400
|
+
const previousCursor = cursor;
|
|
401
|
+
const type = bytes[cursor++];
|
|
402
|
+
const keyEnd = bytes.indexOf(0, cursor);
|
|
403
|
+
if (keyEnd < 0) {
|
|
404
|
+
throw new Error('Invalid BSON: missing cstring terminator');
|
|
405
|
+
}
|
|
406
|
+
const writerOffset = writer.getLength();
|
|
407
|
+
if (!first) {
|
|
408
|
+
writer.writeByte(BYTE_COMMA);
|
|
409
|
+
}
|
|
410
|
+
writer.writeQuotedUtf8Slice(bytes, cursor, keyEnd);
|
|
411
|
+
writer.writeByte(BYTE_COLON);
|
|
412
|
+
cursor = keyEnd + 1;
|
|
413
|
+
const { nextOffset: afterValue, defined } = serializeNestedElementValue(bytes, cursor, type, depth, writer, dateRenderMode);
|
|
414
|
+
cursor = afterValue;
|
|
415
|
+
// Malformed BSON must fail fast instead of getting the parser stuck on the
|
|
416
|
+
// same element forever.
|
|
417
|
+
assertAdvanced(previousCursor, cursor);
|
|
418
|
+
if (!defined) {
|
|
419
|
+
writer.truncate(writerOffset);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
first = false;
|
|
423
|
+
}
|
|
424
|
+
writer.writeByte(BYTE_RBRACE);
|
|
425
|
+
return { nextOffset: offset + totalLength };
|
|
426
|
+
}
|
|
427
|
+
function serializeNestedArrayToJson(bytes, offset, depth, writer, dateRenderMode) {
|
|
428
|
+
if (depth > NESTED_DEPTH_LIMIT) {
|
|
429
|
+
throw new Error(`json nested object depth exceeds the limit of ${NESTED_DEPTH_LIMIT}`);
|
|
430
|
+
}
|
|
431
|
+
const totalLength = readDocumentLength(bytes, offset);
|
|
432
|
+
const bodyEnd = offset + totalLength - 1;
|
|
433
|
+
let cursor = offset + 4;
|
|
434
|
+
writer.writeByte(BYTE_LBRACKET);
|
|
435
|
+
let first = true;
|
|
436
|
+
while (cursor < bodyEnd) {
|
|
437
|
+
const previousCursor = cursor;
|
|
438
|
+
const type = bytes[cursor++];
|
|
439
|
+
cursor = skipCString(bytes, cursor);
|
|
440
|
+
if (!first) {
|
|
441
|
+
writer.writeByte(BYTE_COMMA);
|
|
442
|
+
}
|
|
443
|
+
first = false;
|
|
444
|
+
const { nextOffset: afterValue, defined } = serializeNestedElementValue(bytes, cursor, type, depth, writer, dateRenderMode);
|
|
445
|
+
cursor = afterValue;
|
|
446
|
+
assertAdvanced(previousCursor, cursor);
|
|
447
|
+
if (!defined) {
|
|
448
|
+
writer.writeAscii('null');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
writer.writeByte(BYTE_RBRACKET);
|
|
452
|
+
return { nextOffset: offset + totalLength };
|
|
453
|
+
}
|
|
454
|
+
function serializeNestedElementValue(bytes, offset, type, depth, writer, dateRenderMode) {
|
|
455
|
+
switch (type) {
|
|
456
|
+
case BSON_TYPE_DOUBLE: // Double
|
|
457
|
+
return serializeNestedDoubleElement(bytes, offset, writer);
|
|
458
|
+
case BSON_TYPE_STRING: // String
|
|
459
|
+
return serializeNestedStringElement(bytes, offset, writer);
|
|
460
|
+
case BSON_TYPE_DOCUMENT: // Embedded document
|
|
461
|
+
return serializeNestedObjectElement(bytes, offset, depth, writer, dateRenderMode);
|
|
462
|
+
case BSON_TYPE_ARRAY: // Array
|
|
463
|
+
return serializeNestedArrayElement(bytes, offset, depth, writer, dateRenderMode);
|
|
464
|
+
case BSON_TYPE_BINARY: // Binary
|
|
465
|
+
return serializeNestedBinaryElement(bytes, offset, writer);
|
|
466
|
+
case BSON_TYPE_UNDEFINED: // Undefined
|
|
467
|
+
return { nextOffset: offset, defined: false };
|
|
468
|
+
case BSON_TYPE_OBJECT_ID: {
|
|
469
|
+
// ObjectId
|
|
470
|
+
writer.writeQuotedHexLower(bytes, offset, 12);
|
|
471
|
+
return { nextOffset: offset + 12, defined: true };
|
|
472
|
+
}
|
|
473
|
+
case BSON_TYPE_BOOLEAN: // Boolean
|
|
474
|
+
writer.writeByte(bytes[offset] ? BYTE_ONE : BYTE_ZERO);
|
|
475
|
+
return { nextOffset: offset + 1, defined: true };
|
|
476
|
+
case BSON_TYPE_UTC_DATETIME: // UTC datetime
|
|
477
|
+
return serializeNestedDateTimeElement(bytes, offset, writer, dateRenderMode);
|
|
478
|
+
case BSON_TYPE_NULL: // Null
|
|
479
|
+
case BSON_TYPE_MIN_KEY: // MinKey
|
|
480
|
+
case BSON_TYPE_MAX_KEY: // MaxKey
|
|
481
|
+
writer.writeAscii('null');
|
|
482
|
+
return { nextOffset: offset, defined: true };
|
|
483
|
+
case BSON_TYPE_REGEX: // Regular expression
|
|
484
|
+
return serializeNestedRegexElement(bytes, offset, writer);
|
|
485
|
+
case BSON_TYPE_DB_POINTER: // DBPointer
|
|
486
|
+
return serializeNestedDbPointerElement(bytes, offset, writer);
|
|
487
|
+
case BSON_TYPE_CODE: // JavaScript code
|
|
488
|
+
return serializeNestedCodeElement(bytes, offset, depth, writer);
|
|
489
|
+
case BSON_TYPE_SYMBOL: // Symbol
|
|
490
|
+
return serializeNestedSymbolElement(bytes, offset, writer);
|
|
491
|
+
case BSON_TYPE_CODE_WITH_SCOPE: // JavaScript code with scope
|
|
492
|
+
return serializeNestedCodeWithScopeElement(bytes, offset, depth, writer, dateRenderMode);
|
|
493
|
+
case BSON_TYPE_INT32: {
|
|
494
|
+
// Int32
|
|
495
|
+
writer.writeAscii(String(readInt32LE(bytes, offset)));
|
|
496
|
+
return { nextOffset: offset + 4, defined: true };
|
|
497
|
+
}
|
|
498
|
+
case BSON_TYPE_TIMESTAMP: {
|
|
499
|
+
// Timestamp
|
|
500
|
+
writer.writeAscii(timestampToBigInt(bytes, offset).toString());
|
|
501
|
+
return { nextOffset: offset + 8, defined: true };
|
|
502
|
+
}
|
|
503
|
+
case BSON_TYPE_INT64: {
|
|
504
|
+
// Int64
|
|
505
|
+
writer.writeAscii(bytes.readBigInt64LE(offset).toString());
|
|
506
|
+
return { nextOffset: offset + 8, defined: true };
|
|
507
|
+
}
|
|
508
|
+
case BSON_TYPE_DECIMAL128: // Decimal128
|
|
509
|
+
writer.writeQuotedJsonString(decimal128ToString(bytes, offset));
|
|
510
|
+
return { nextOffset: offset + 16, defined: true };
|
|
511
|
+
default:
|
|
512
|
+
throw new Error(`Unsupported BSON nested type: 0x${type.toString(16)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function serializeNestedDoubleElement(bytes, offset, writer) {
|
|
516
|
+
const value = bytes.readDoubleLE(offset);
|
|
517
|
+
if (!Number.isFinite(value)) {
|
|
518
|
+
writer.writeAscii('null');
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
writer.writeAscii(value.toString());
|
|
522
|
+
}
|
|
523
|
+
return { nextOffset: offset + 8, defined: true };
|
|
524
|
+
}
|
|
525
|
+
function serializeNestedStringElement(bytes, offset, writer) {
|
|
526
|
+
const nextOffset = readBsonStringEnd(bytes, offset);
|
|
527
|
+
const stringStart = offset + 4;
|
|
528
|
+
const length = nextOffset - stringStart;
|
|
529
|
+
writer.writeQuotedUtf8Slice(bytes, stringStart, stringStart + length - 1);
|
|
530
|
+
return { nextOffset, defined: true };
|
|
531
|
+
}
|
|
532
|
+
function serializeNestedObjectElement(bytes, offset, depth, writer, dateRenderMode) {
|
|
533
|
+
const result = serializeNestedObjectToJson(bytes, offset, depth + 1, writer, dateRenderMode);
|
|
534
|
+
return { nextOffset: result.nextOffset, defined: true };
|
|
535
|
+
}
|
|
536
|
+
function serializeNestedArrayElement(bytes, offset, depth, writer, dateRenderMode) {
|
|
537
|
+
const result = serializeNestedArrayToJson(bytes, offset, depth + 1, writer, dateRenderMode);
|
|
538
|
+
return { nextOffset: result.nextOffset, defined: true };
|
|
539
|
+
}
|
|
540
|
+
function serializeNestedDateTimeElement(bytes, offset, writer, dateRenderMode) {
|
|
541
|
+
appendDateTimeToWriter(writer, Number(bytes.readBigInt64LE(offset)), true, dateRenderMode);
|
|
542
|
+
return { nextOffset: offset + 8, defined: true };
|
|
543
|
+
}
|
|
544
|
+
function serializeNestedRegexElement(bytes, offset, writer) {
|
|
545
|
+
const { pattern, options, nextOffset } = parseRegex(bytes, offset);
|
|
546
|
+
writeRegexJson(writer, pattern, options);
|
|
547
|
+
return { nextOffset, defined: true };
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* KLUDGE: The DateTimeValue API needs a CompatibilityContext, but we don't want to pass that
|
|
551
|
+
* around through the entire stack when the DateRenderMode encapsulates it.
|
|
552
|
+
*
|
|
553
|
+
* This translates back from DateRenderMode to CompatibilityContext.
|
|
554
|
+
*/
|
|
555
|
+
const DATETIME_COMPATIBILITY_OPTIONS = {
|
|
556
|
+
[0 /* DateRenderMode.LEGACY_MILLISECONDS */]: CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY,
|
|
557
|
+
[1 /* DateRenderMode.ISO_MILLISECONDS */]: new CompatibilityContext({
|
|
558
|
+
edition: 2,
|
|
559
|
+
maxTimeValuePrecision: TimeValuePrecision.milliseconds
|
|
560
|
+
}),
|
|
561
|
+
[2 /* DateRenderMode.ISO_SECONDS */]: new CompatibilityContext({
|
|
562
|
+
edition: 2,
|
|
563
|
+
maxTimeValuePrecision: TimeValuePrecision.seconds
|
|
564
|
+
})
|
|
565
|
+
};
|
|
566
|
+
const MONGO_TIME_OPTIONS = {
|
|
567
|
+
subSecondPrecision: TimeValuePrecision.milliseconds,
|
|
568
|
+
defaultSubSecondPrecision: TimeValuePrecision.milliseconds
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* Fallback date serialization.
|
|
572
|
+
*
|
|
573
|
+
* This is slow, but handles edge cases.
|
|
574
|
+
*/
|
|
575
|
+
function extendedDateTimeString(millis, dateRenderMode) {
|
|
576
|
+
const isoString = new Date(millis).toISOString();
|
|
577
|
+
const compatibilityContext = DATETIME_COMPATIBILITY_OPTIONS[dateRenderMode];
|
|
578
|
+
return new DateTimeValue(isoString, undefined, MONGO_TIME_OPTIONS).toSqliteValue(compatibilityContext);
|
|
579
|
+
}
|
|
580
|
+
export function getDateRenderMode(compatibilityContext) {
|
|
581
|
+
if (!compatibilityContext.isEnabled(CompatibilityOption.timestampsIso8601)) {
|
|
582
|
+
return 0 /* DateRenderMode.LEGACY_MILLISECONDS */;
|
|
583
|
+
}
|
|
584
|
+
const maxPrecision = compatibilityContext.maxTimeValuePrecision ?? TimeValuePrecision.milliseconds;
|
|
585
|
+
if (maxPrecision === TimeValuePrecision.seconds) {
|
|
586
|
+
return 2 /* DateRenderMode.ISO_SECONDS */;
|
|
587
|
+
}
|
|
588
|
+
// MongoDB only supports millisecond precision, so this also convers configured values of
|
|
589
|
+
// microseconds and nanoseconds.
|
|
590
|
+
return 1 /* DateRenderMode.ISO_MILLISECONDS */;
|
|
591
|
+
}
|
|
592
|
+
function appendDateTimeToWriter(writer, millis, quoted, dateRenderMode) {
|
|
593
|
+
const date = SHARED_UTC_DATE;
|
|
594
|
+
date.setTime(millis);
|
|
595
|
+
if (Number.isNaN(date.getTime())) {
|
|
596
|
+
throw new RangeError('Invalid time value');
|
|
597
|
+
}
|
|
598
|
+
const year = date.getUTCFullYear();
|
|
599
|
+
if (year < 0 || year > 9999) {
|
|
600
|
+
// Abnormal date ranges. We support these, but don't optimize for performance.
|
|
601
|
+
const string = extendedDateTimeString(millis, dateRenderMode);
|
|
602
|
+
if (quoted) {
|
|
603
|
+
writer.writeQuotedJsonString(string);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
writer.writeUtf8(string);
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
writer.writeDateTime(year, date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds(), quoted, dateRenderMode === 0 /* DateRenderMode.LEGACY_MILLISECONDS */ ? BYTE_SPACE : BYTE_T, dateRenderMode !== 2 /* DateRenderMode.ISO_SECONDS */);
|
|
611
|
+
}
|
|
612
|
+
function hexLower(bytes, offset, length) {
|
|
613
|
+
return bytes.toString('hex', offset, offset + length);
|
|
614
|
+
}
|
|
615
|
+
function writeRegexJson(writer, pattern, options) {
|
|
616
|
+
writer.writeAscii('{"pattern":');
|
|
617
|
+
writer.writeQuotedJsonString(pattern);
|
|
618
|
+
writer.writeAscii(',"options":');
|
|
619
|
+
writer.writeQuotedJsonString(options);
|
|
620
|
+
writer.writeByte(BYTE_RBRACE);
|
|
621
|
+
}
|
|
622
|
+
function writeCodeJson(bytes, offset, depth, writer) {
|
|
623
|
+
const { value: code, nextOffset } = readBsonString(bytes, offset);
|
|
624
|
+
writer.writeAscii('{"code":');
|
|
625
|
+
writer.writeQuotedJsonString(code);
|
|
626
|
+
writer.writeAscii(',"scope":null}');
|
|
627
|
+
return nextOffset;
|
|
628
|
+
}
|
|
629
|
+
function writeCodeWithScopeJson(bytes, offset, depth, writer, dateRenderMode) {
|
|
630
|
+
const totalLength = readInt32LE(bytes, offset);
|
|
631
|
+
const { value: code, nextOffset: afterCode } = readBsonString(bytes, offset + 4);
|
|
632
|
+
writer.writeAscii('{"code":');
|
|
633
|
+
writer.writeQuotedJsonString(code);
|
|
634
|
+
writer.writeAscii(',"scope":');
|
|
635
|
+
serializeNestedObjectToJson(bytes, afterCode, depth + 1, writer, dateRenderMode);
|
|
636
|
+
writer.writeByte(BYTE_RBRACE);
|
|
637
|
+
// code_w_scope carries its own total byte length, so we trust that wrapper
|
|
638
|
+
// rather than reconstructing the end position from the nested scope.
|
|
639
|
+
return offset + totalLength;
|
|
640
|
+
}
|
|
641
|
+
function writeDbPointerJson(bytes, offset, writer) {
|
|
642
|
+
const { value: collection, nextOffset } = readBsonString(bytes, offset);
|
|
643
|
+
const separator = collection.indexOf('.');
|
|
644
|
+
const db = separator >= 0 ? collection.slice(0, separator) : null;
|
|
645
|
+
const collectionName = separator >= 0 ? collection.slice(separator + 1) : collection;
|
|
646
|
+
writer.writeAscii('{"collection":');
|
|
647
|
+
writer.writeQuotedJsonString(collectionName);
|
|
648
|
+
writer.writeAscii(',"oid":');
|
|
649
|
+
writer.writeQuotedHexLower(bytes, nextOffset, 12);
|
|
650
|
+
if (db != null) {
|
|
651
|
+
writer.writeAscii(',"db":');
|
|
652
|
+
writer.writeQuotedJsonString(db);
|
|
653
|
+
}
|
|
654
|
+
writer.writeAscii(',"fields":{}}');
|
|
655
|
+
return nextOffset + 12;
|
|
656
|
+
}
|
|
657
|
+
function serializeNestedBinaryElement(bytes, offset, writer) {
|
|
658
|
+
const length = readInt32LE(bytes, offset);
|
|
659
|
+
if (length < 0) {
|
|
660
|
+
throw new Error('Invalid BSON binary length');
|
|
661
|
+
}
|
|
662
|
+
const subtype = bytes[offset + 4];
|
|
663
|
+
const dataStart = offset + 5;
|
|
664
|
+
const dataEnd = dataStart + length;
|
|
665
|
+
if (dataEnd > bytes.length) {
|
|
666
|
+
throw new Error('Invalid BSON binary length');
|
|
667
|
+
}
|
|
668
|
+
const slice = binaryDataSlice(bytes, dataStart, dataEnd, subtype);
|
|
669
|
+
// Nested binary values are omitted from JSON unless they are subtype 4 UUIDs,
|
|
670
|
+
// which are represented as strings for parity with the default path.
|
|
671
|
+
if (subtype === BSON_BINARY_SUBTYPE_UUID && slice.length === 16) {
|
|
672
|
+
writer.writeQuotedUuid(slice, 0);
|
|
673
|
+
return { nextOffset: dataEnd, defined: true };
|
|
674
|
+
}
|
|
675
|
+
return { nextOffset: dataEnd, defined: false };
|
|
676
|
+
}
|
|
677
|
+
function assertAdvanced(previousOffset, nextOffset) {
|
|
678
|
+
if (nextOffset <= previousOffset) {
|
|
679
|
+
throw new Error('Invalid BSON parser state: non-advancing offset');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function serializeNestedDbPointerElement(bytes, offset, writer) {
|
|
683
|
+
return { nextOffset: writeDbPointerJson(bytes, offset, writer), defined: true };
|
|
684
|
+
}
|
|
685
|
+
function serializeNestedCodeElement(bytes, offset, depth, writer) {
|
|
686
|
+
return { nextOffset: writeCodeJson(bytes, offset, depth, writer), defined: true };
|
|
687
|
+
}
|
|
688
|
+
function serializeNestedSymbolElement(bytes, offset, writer) {
|
|
689
|
+
return serializeNestedStringElement(bytes, offset, writer);
|
|
690
|
+
}
|
|
691
|
+
function serializeNestedCodeWithScopeElement(bytes, offset, depth, writer, dateRenderMode) {
|
|
692
|
+
return {
|
|
693
|
+
nextOffset: writeCodeWithScopeJson(bytes, offset, depth, writer, dateRenderMode),
|
|
694
|
+
defined: true
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const idKey = Buffer.from('_id');
|
|
698
|
+
/**
|
|
699
|
+
* Parse an _id from a buffer, without parsing the entire document.
|
|
700
|
+
*
|
|
701
|
+
* The parsed _id is parsed using standard bson.deserialize - different from bufferToSqlite.
|
|
702
|
+
*
|
|
703
|
+
* @returns the parsed id, as well as a serialized document including only _id.
|
|
704
|
+
*/
|
|
705
|
+
export function parseDocumentId(bytes) {
|
|
706
|
+
const bodyEnd = readDocumentLength(bytes, 0) - 1;
|
|
707
|
+
let offset = 4;
|
|
708
|
+
while (offset < bodyEnd) {
|
|
709
|
+
const baseOffset = offset;
|
|
710
|
+
const type = bytes[baseOffset];
|
|
711
|
+
// In most cases the first key should be _id, but we also handle cases where
|
|
712
|
+
// it occurs later.
|
|
713
|
+
const keyStart = baseOffset + 1;
|
|
714
|
+
const afterKey = skipCString(bytes, keyStart);
|
|
715
|
+
const keyEnd = afterKey - 1; // without null terminator
|
|
716
|
+
const nextOffset = skipBsonValue(bytes, afterKey, type);
|
|
717
|
+
offset = nextOffset;
|
|
718
|
+
if (keyEnd - keyStart != 3) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (!idKey.equals(bytes.subarray(keyStart, keyEnd))) {
|
|
722
|
+
// Not _id - check the next key
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
// We create a new "document" containing only the _id, by directly manipulating buffers.
|
|
726
|
+
// https://bsonspec.org/spec.html
|
|
727
|
+
// document ::= int32 e_list unsigned_byte(0)
|
|
728
|
+
// e_list ::= element e_list
|
|
729
|
+
// element ::= signed_byte e_name ...
|
|
730
|
+
const baseLength = nextOffset - baseOffset;
|
|
731
|
+
// Our buffer wraps the _id element: 4 bytes before for the size, 1 null byte at the end.
|
|
732
|
+
const genBuffer = Buffer.allocUnsafe(baseLength + 5);
|
|
733
|
+
genBuffer.writeInt32LE(baseLength + 5, 0);
|
|
734
|
+
bytes.copy(genBuffer, 4, baseOffset, baseOffset + baseLength);
|
|
735
|
+
genBuffer[genBuffer.length - 1] = 0;
|
|
736
|
+
return { idBuffer: genBuffer, id: mongo.BSON.deserialize(genBuffer, { useBigInt64: true })._id };
|
|
737
|
+
}
|
|
738
|
+
throw new Error(`Attempt to parse document without _id`);
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=bufferToSqlite.js.map
|