@powersync/service-module-mongodb 0.15.3 → 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 +52 -0
- package/dist/api/MongoRouteAPIAdapter.js +2 -2
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- 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/ChangeStreamReplicator.d.ts +1 -1
- package/dist/replication/ChangeStreamReplicator.js +1 -1
- package/dist/replication/ChangeStreamReplicator.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/MongoManager.d.ts +1 -1
- package/dist/replication/MongoManager.js +1 -1
- package/dist/replication/MongoManager.js.map +1 -1
- 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 +4 -2
- package/dist/replication/replication-index.js +4 -2
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/replication/replication-utils.d.ts +1 -1
- package/dist/types/types.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/api/MongoRouteAPIAdapter.ts +2 -2
- package/src/replication/ChangeStream.ts +348 -371
- package/src/replication/ChangeStreamReplicationJob.ts +2 -2
- package/src/replication/ChangeStreamReplicator.ts +2 -5
- package/src/replication/JsonBufferWriter.ts +390 -0
- package/src/replication/MongoManager.ts +2 -2
- package/src/replication/MongoRelation.ts +5 -2
- 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 -66
- package/src/replication/replication-index.ts +4 -2
- package/src/replication/replication-utils.ts +2 -2
- package/src/types/types.ts +1 -1
- package/test/src/buffer_to_sqlite.test.ts +1146 -0
- package/test/src/change_stream.test.ts +49 -3
- 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
|
@@ -10,7 +10,6 @@ import { MongoRouteAPIAdapter } from '@module/api/MongoRouteAPIAdapter.js';
|
|
|
10
10
|
import { PostImagesOption } from '@module/types/types.js';
|
|
11
11
|
import { ChangeStreamTestContext } from './change_stream_utils.js';
|
|
12
12
|
import { describeWithStorage, StorageVersionTestContext, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
13
|
-
import { createCheckpoint, STANDALONE_CHECKPOINT_ID } from '@module/replication/MongoRelation.js';
|
|
14
13
|
|
|
15
14
|
const BASIC_SYNC_RULES = `
|
|
16
15
|
bucket_definitions:
|
|
@@ -258,6 +257,53 @@ bucket_definitions:
|
|
|
258
257
|
expect(data).toMatchObject([test_utils.putOp('test_DATA', { id: test_id, description: 'test1' })]);
|
|
259
258
|
});
|
|
260
259
|
|
|
260
|
+
test('replicating from multiple databases in the same cluster', async () => {
|
|
261
|
+
await using context = await openContext();
|
|
262
|
+
const { client, db } = context;
|
|
263
|
+
const otherDb = client.db(`${db.databaseName}_other_${storageVersion}`);
|
|
264
|
+
await otherDb.dropDatabase();
|
|
265
|
+
await using _ = {
|
|
266
|
+
[Symbol.asyncDispose]: async () => {
|
|
267
|
+
await otherDb.dropDatabase();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
await context.updateSyncRules(`
|
|
272
|
+
bucket_definitions:
|
|
273
|
+
global:
|
|
274
|
+
data:
|
|
275
|
+
- SELECT _id as id, description FROM "${db.databaseName}"."test_data_default"
|
|
276
|
+
- SELECT _id as id, description FROM "${otherDb.databaseName}"."test_data_other"
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
await db.createCollection('test_data_default');
|
|
280
|
+
await otherDb.createCollection('test_data_other');
|
|
281
|
+
await context.replicateSnapshot();
|
|
282
|
+
context.startStreaming();
|
|
283
|
+
|
|
284
|
+
const defaultResult = await db.collection('test_data_default').insertOne({ description: 'default db' });
|
|
285
|
+
const otherResult = await otherDb.collection('test_data_other').insertOne({ description: 'other db' });
|
|
286
|
+
|
|
287
|
+
const data = await context.getBucketData('global[]');
|
|
288
|
+
|
|
289
|
+
expect(data).toEqual(
|
|
290
|
+
expect.arrayContaining([
|
|
291
|
+
expect.objectContaining(
|
|
292
|
+
test_utils.putOp('test_data_default', {
|
|
293
|
+
id: defaultResult.insertedId.toHexString(),
|
|
294
|
+
description: 'default db'
|
|
295
|
+
})
|
|
296
|
+
),
|
|
297
|
+
expect.objectContaining(
|
|
298
|
+
test_utils.putOp('test_data_other', {
|
|
299
|
+
id: otherResult.insertedId.toHexString(),
|
|
300
|
+
description: 'other db'
|
|
301
|
+
})
|
|
302
|
+
)
|
|
303
|
+
])
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
261
307
|
test('replicating large values', async () => {
|
|
262
308
|
await using context = await openContext();
|
|
263
309
|
const { db } = context;
|
|
@@ -450,7 +496,7 @@ bucket_definitions:
|
|
|
450
496
|
// The field appears twice in the ChangeStream event, so the total size
|
|
451
497
|
// is > 16MB.
|
|
452
498
|
|
|
453
|
-
// We don't actually have this description field in the sync
|
|
499
|
+
// We don't actually have this description field in the sync config,
|
|
454
500
|
// That causes other issues, not relevant for this specific test.
|
|
455
501
|
const largeDescription = crypto.randomBytes(12000000 / 2).toString('hex');
|
|
456
502
|
|
|
@@ -479,7 +525,7 @@ bucket_definitions:
|
|
|
479
525
|
});
|
|
480
526
|
});
|
|
481
527
|
|
|
482
|
-
test('collection not in sync
|
|
528
|
+
test('collection not in sync config', async () => {
|
|
483
529
|
await using context = await openContext();
|
|
484
530
|
const { db } = context;
|
|
485
531
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
ProtocolOpId,
|
|
10
10
|
ReplicationCheckpoint,
|
|
11
11
|
storage,
|
|
12
|
-
STORAGE_VERSION_CONFIG,
|
|
13
12
|
SyncRulesBucketStorage,
|
|
14
13
|
TestStorageOptions,
|
|
15
14
|
updateSyncRulesFromYaml,
|
|
@@ -28,7 +27,6 @@ export class ChangeStreamTestContext {
|
|
|
28
27
|
private _walStream?: ChangeStream;
|
|
29
28
|
private abortController = new AbortController();
|
|
30
29
|
private streamPromise?: Promise<PromiseSettledResult<void>>;
|
|
31
|
-
private syncRulesId?: number;
|
|
32
30
|
private syncRulesContent?: storage.PersistedSyncRulesContent;
|
|
33
31
|
public storage?: SyncRulesBucketStorage;
|
|
34
32
|
|
|
@@ -55,17 +53,15 @@ export class ChangeStreamTestContext {
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
|
|
58
|
-
const versionedBuckets = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false;
|
|
59
56
|
|
|
60
|
-
return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions, storageVersion
|
|
57
|
+
return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions, storageVersion);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
constructor(
|
|
64
61
|
public factory: BucketStorageFactory,
|
|
65
62
|
public connectionManager: MongoManager,
|
|
66
63
|
private streamOptions: Partial<ChangeStreamOptions> = {},
|
|
67
|
-
private storageVersion: number = LEGACY_STORAGE_VERSION
|
|
68
|
-
private versionedBuckets: boolean = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false
|
|
64
|
+
private storageVersion: number = LEGACY_STORAGE_VERSION
|
|
69
65
|
) {
|
|
70
66
|
createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
71
67
|
initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
@@ -105,7 +101,6 @@ export class ChangeStreamTestContext {
|
|
|
105
101
|
const syncRules = await this.factory.updateSyncRules(
|
|
106
102
|
updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
|
|
107
103
|
);
|
|
108
|
-
this.syncRulesId = syncRules.id;
|
|
109
104
|
this.syncRulesContent = syncRules;
|
|
110
105
|
this.storage = this.factory.getInstance(syncRules);
|
|
111
106
|
return this.storage!;
|
|
@@ -114,10 +109,9 @@ export class ChangeStreamTestContext {
|
|
|
114
109
|
async loadNextSyncRules() {
|
|
115
110
|
const syncRules = await this.factory.getNextSyncRulesContent();
|
|
116
111
|
if (syncRules == null) {
|
|
117
|
-
throw new Error(`Next sync
|
|
112
|
+
throw new Error(`Next sync config not available`);
|
|
118
113
|
}
|
|
119
114
|
|
|
120
|
-
this.syncRulesId = syncRules.id;
|
|
121
115
|
this.syncRulesContent = syncRules;
|
|
122
116
|
this.storage = this.factory.getInstance(syncRules);
|
|
123
117
|
return this.storage!;
|
|
@@ -125,7 +119,7 @@ export class ChangeStreamTestContext {
|
|
|
125
119
|
|
|
126
120
|
private getSyncRulesContent(): storage.PersistedSyncRulesContent {
|
|
127
121
|
if (this.syncRulesContent == null) {
|
|
128
|
-
throw new Error('Sync
|
|
122
|
+
throw new Error('Sync config not configured - call updateSyncRules() first');
|
|
129
123
|
}
|
|
130
124
|
return this.syncRulesContent;
|
|
131
125
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import {
|
|
3
|
-
applyRowContext,
|
|
4
3
|
CompatibilityContext,
|
|
5
4
|
CompatibilityEdition,
|
|
6
|
-
SqliteInputRow,
|
|
7
5
|
SqlSyncRules,
|
|
8
6
|
TimeValuePrecision
|
|
9
7
|
} from '@powersync/service-sync-rules';
|
|
10
8
|
import { describe, expect, test } from 'vitest';
|
|
11
9
|
|
|
12
10
|
import { MongoRouteAPIAdapter } from '@module/api/MongoRouteAPIAdapter.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { parseDocumentId } from '@module/replication/bufferToSqlite.js';
|
|
12
|
+
import { ChangeStreamBatch, parseChangeDocument, rawChangeStream } from '@module/replication/RawChangeStream.js';
|
|
13
|
+
import { DirectSourceRowConverter } from '@module/replication/SourceRowConverter.js';
|
|
15
14
|
import { PostImagesOption } from '@module/types/types.js';
|
|
16
15
|
import { clearTestDb, connectMongoData, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
17
16
|
|
|
18
17
|
describe('mongo data types', () => {
|
|
18
|
+
// These test the full data cycle by writing to mongodb, then checking the change stream and direct collection queries.
|
|
19
|
+
// More direct tests directly on the BSON values are in buffer_to_sqlite.test.ts.
|
|
20
|
+
|
|
19
21
|
async function setupTable(db: mongo.Db) {
|
|
20
22
|
await clearTestDb(db);
|
|
21
23
|
}
|
|
@@ -46,14 +48,7 @@ describe('mongo data types', () => {
|
|
|
46
48
|
maxKey: new mongo.MaxKey(),
|
|
47
49
|
symbol: new mongo.BSONSymbol('test'),
|
|
48
50
|
js: new mongo.Code('testcode'),
|
|
49
|
-
js2: new mongo.Code('testcode', { foo: 'bar' })
|
|
50
|
-
pointer: new mongo.DBRef('mycollection', mongo.ObjectId.createFromHexString('66e834cc91d805df11fa0ecb')),
|
|
51
|
-
pointer2: new mongo.DBRef(
|
|
52
|
-
'mycollection',
|
|
53
|
-
mongo.ObjectId.createFromHexString('66e834cc91d805df11fa0ecb'),
|
|
54
|
-
'mydb',
|
|
55
|
-
{ foo: 'bar' }
|
|
56
|
-
)
|
|
51
|
+
js2: new mongo.Code('testcode', { foo: 'bar' })
|
|
57
52
|
},
|
|
58
53
|
{
|
|
59
54
|
_id: 6 as any,
|
|
@@ -139,14 +134,14 @@ describe('mongo data types', () => {
|
|
|
139
134
|
maxKey: [new mongo.MaxKey()],
|
|
140
135
|
symbol: [new mongo.BSONSymbol('test')],
|
|
141
136
|
js: [new mongo.Code('testcode')],
|
|
142
|
-
pointer: [new mongo.DBRef('mycollection', mongo.ObjectId.createFromHexString('66e834cc91d805df11fa0ecb'))],
|
|
143
137
|
undefined: [undefined]
|
|
144
138
|
}
|
|
145
139
|
]);
|
|
146
140
|
}
|
|
147
141
|
|
|
148
|
-
function checkResults(
|
|
149
|
-
const
|
|
142
|
+
function checkResults(documents: Buffer[]) {
|
|
143
|
+
const converter = new DirectSourceRowConverter(CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY);
|
|
144
|
+
const sqliteValue = documents.map((d) => converter.rawToSqliteRow(d).row);
|
|
150
145
|
|
|
151
146
|
expect(sqliteValue[0]).toMatchObject({
|
|
152
147
|
_id: 1n,
|
|
@@ -180,9 +175,7 @@ describe('mongo data types', () => {
|
|
|
180
175
|
maxKey: null,
|
|
181
176
|
symbol: 'test',
|
|
182
177
|
js: '{"code":"testcode","scope":null}',
|
|
183
|
-
js2: '{"code":"testcode","scope":{"foo":"bar"}}'
|
|
184
|
-
pointer: '{"collection":"mycollection","oid":"66e834cc91d805df11fa0ecb","fields":{}}',
|
|
185
|
-
pointer2: '{"collection":"mycollection","oid":"66e834cc91d805df11fa0ecb","db":"mydb","fields":{"foo":"bar"}}'
|
|
178
|
+
js2: '{"code":"testcode","scope":{"foo":"bar"}}'
|
|
186
179
|
});
|
|
187
180
|
|
|
188
181
|
// This must specifically be null, and not undefined.
|
|
@@ -197,8 +190,9 @@ describe('mongo data types', () => {
|
|
|
197
190
|
});
|
|
198
191
|
}
|
|
199
192
|
|
|
200
|
-
function checkResultsNested(
|
|
201
|
-
const
|
|
193
|
+
function checkResultsNested(documents: Buffer[]) {
|
|
194
|
+
const converter = new DirectSourceRowConverter(CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY);
|
|
195
|
+
const sqliteValue = documents.map((d) => converter.rawToSqliteRow(d).row);
|
|
202
196
|
|
|
203
197
|
expect(sqliteValue[0]).toMatchObject({
|
|
204
198
|
_id: 1n,
|
|
@@ -245,7 +239,6 @@ describe('mongo data types', () => {
|
|
|
245
239
|
regexp: '[{"pattern":"test","options":"i"}]',
|
|
246
240
|
symbol: '["test"]',
|
|
247
241
|
js: '[{"code":"testcode","scope":null}]',
|
|
248
|
-
pointer: '[{"collection":"mycollection","oid":"66e834cc91d805df11fa0ecb","fields":{}}]',
|
|
249
242
|
minKey: '[null]',
|
|
250
243
|
maxKey: '[null]'
|
|
251
244
|
});
|
|
@@ -262,12 +255,11 @@ describe('mongo data types', () => {
|
|
|
262
255
|
|
|
263
256
|
const rawResults = await db
|
|
264
257
|
.collection('test_data')
|
|
265
|
-
.find({}, { sort: { _id: 1 } })
|
|
258
|
+
.find<Buffer>({}, { sort: { _id: 1 }, raw: true })
|
|
266
259
|
.toArray();
|
|
267
260
|
// It is tricky to save "undefined" with mongo, so we check that it succeeded.
|
|
268
|
-
expect(rawResults[4].undefined).toBeUndefined();
|
|
269
|
-
|
|
270
|
-
checkResults(transformed);
|
|
261
|
+
expect(mongo.BSON.deserialize(rawResults[4]).undefined).toBeUndefined();
|
|
262
|
+
checkResults(rawResults);
|
|
271
263
|
} finally {
|
|
272
264
|
await client.close();
|
|
273
265
|
}
|
|
@@ -284,12 +276,11 @@ describe('mongo data types', () => {
|
|
|
284
276
|
|
|
285
277
|
const rawResults = await db
|
|
286
278
|
.collection('test_data_arrays')
|
|
287
|
-
.find({}, { sort: { _id: 1 } })
|
|
279
|
+
.find<Buffer>({}, { sort: { _id: 1 }, raw: true })
|
|
288
280
|
.toArray();
|
|
289
|
-
expect(rawResults[3].undefined).toEqual([undefined]);
|
|
290
|
-
const transformed = [...ChangeStream.getQueryData(rawResults)];
|
|
281
|
+
expect(mongo.BSON.deserialize(rawResults[3]).undefined).toEqual([undefined]);
|
|
291
282
|
|
|
292
|
-
checkResultsNested(
|
|
283
|
+
checkResultsNested(rawResults);
|
|
293
284
|
} finally {
|
|
294
285
|
await client.close();
|
|
295
286
|
}
|
|
@@ -303,19 +294,18 @@ describe('mongo data types', () => {
|
|
|
303
294
|
try {
|
|
304
295
|
await setupTable(db);
|
|
305
296
|
|
|
306
|
-
const stream = db
|
|
297
|
+
const stream = rawChangeStream(db, [{ $changeStream: { fullDocument: 'updateLookup' } }], {
|
|
307
298
|
maxAwaitTimeMS: 50,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
await stream.
|
|
299
|
+
maxTimeMS: 1000,
|
|
300
|
+
batchSize: 10
|
|
301
|
+
})[Symbol.asyncIterator]();
|
|
302
|
+
await stream.next();
|
|
312
303
|
|
|
313
304
|
await insert(collection);
|
|
314
305
|
await insertUndefined(db, 'test_data');
|
|
315
306
|
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
checkResults(transformed);
|
|
307
|
+
const documents = await getReplicationTx(stream, 6);
|
|
308
|
+
checkResults(documents);
|
|
319
309
|
} finally {
|
|
320
310
|
await client.close();
|
|
321
311
|
}
|
|
@@ -327,19 +317,18 @@ describe('mongo data types', () => {
|
|
|
327
317
|
try {
|
|
328
318
|
await setupTable(db);
|
|
329
319
|
|
|
330
|
-
const stream = db
|
|
320
|
+
const stream = rawChangeStream(db, [{ $changeStream: { fullDocument: 'updateLookup' } }], {
|
|
331
321
|
maxAwaitTimeMS: 50,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
await stream.
|
|
322
|
+
maxTimeMS: 1000,
|
|
323
|
+
batchSize: 10
|
|
324
|
+
})[Symbol.asyncIterator]();
|
|
325
|
+
await stream.next();
|
|
336
326
|
|
|
337
327
|
await insertNested(collection);
|
|
338
328
|
await insertUndefined(db, 'test_data_arrays', true);
|
|
339
329
|
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
checkResultsNested(transformed);
|
|
330
|
+
const documents = await getReplicationTx(stream, 6);
|
|
331
|
+
checkResultsNested(documents);
|
|
343
332
|
} finally {
|
|
344
333
|
await client.close();
|
|
345
334
|
}
|
|
@@ -382,9 +371,6 @@ describe('mongo data types', () => {
|
|
|
382
371
|
{ name: 'nested', sqlite_type: 2, internal_type: 'Object' },
|
|
383
372
|
{ name: 'null', sqlite_type: 0, internal_type: 'Null' },
|
|
384
373
|
{ name: 'objectId', sqlite_type: 2, internal_type: 'ObjectId' },
|
|
385
|
-
// We can fix these later
|
|
386
|
-
{ name: 'pointer', sqlite_type: 2, internal_type: 'Object' },
|
|
387
|
-
{ name: 'pointer2', sqlite_type: 2, internal_type: 'Object' },
|
|
388
374
|
{ name: 'regexp', sqlite_type: 2, internal_type: 'RegExp' },
|
|
389
375
|
// Can fix this later
|
|
390
376
|
{ name: 'symbol', sqlite_type: 2, internal_type: 'String' },
|
|
@@ -548,27 +534,31 @@ bucket_definitions:
|
|
|
548
534
|
.collection('test_data')
|
|
549
535
|
.find({}, { sort: { _id: 1 } })
|
|
550
536
|
.toArray();
|
|
551
|
-
const [row] =
|
|
537
|
+
const [row] = rawResults;
|
|
552
538
|
|
|
553
|
-
const oldFormat =
|
|
539
|
+
const oldFormat = new DirectSourceRowConverter(
|
|
540
|
+
CompatibilityContext.FULL_BACKWARDS_COMPATIBILITY
|
|
541
|
+
).documentToSqliteRow(row);
|
|
554
542
|
expect(oldFormat).toMatchObject({
|
|
555
543
|
fraction: '2023-03-06 13:47:01.123Z',
|
|
556
544
|
noFraction: '2023-03-06 13:47:01.000Z'
|
|
557
545
|
});
|
|
558
546
|
|
|
559
|
-
const newFormat =
|
|
547
|
+
const newFormat = new DirectSourceRowConverter(
|
|
548
|
+
new CompatibilityContext({ edition: CompatibilityEdition.SYNC_STREAMS })
|
|
549
|
+
).documentToSqliteRow(row);
|
|
560
550
|
expect(newFormat).toMatchObject({
|
|
561
551
|
fraction: '2023-03-06T13:47:01.123Z',
|
|
562
552
|
noFraction: '2023-03-06T13:47:01.000Z'
|
|
563
553
|
});
|
|
564
554
|
|
|
565
|
-
const reducedPrecisionFormat =
|
|
566
|
-
row,
|
|
555
|
+
const reducedPrecisionFormat = new DirectSourceRowConverter(
|
|
567
556
|
new CompatibilityContext({
|
|
568
557
|
edition: CompatibilityEdition.SYNC_STREAMS,
|
|
569
558
|
maxTimeValuePrecision: TimeValuePrecision.seconds
|
|
570
559
|
})
|
|
571
|
-
);
|
|
560
|
+
).documentToSqliteRow(row);
|
|
561
|
+
|
|
572
562
|
expect(reducedPrecisionFormat).toMatchObject({
|
|
573
563
|
fraction: '2023-03-06T13:47:01Z',
|
|
574
564
|
noFraction: '2023-03-06T13:47:01Z'
|
|
@@ -582,18 +572,30 @@ bucket_definitions:
|
|
|
582
572
|
/**
|
|
583
573
|
* Return all the inserts from the first transaction in the replication stream.
|
|
584
574
|
*/
|
|
585
|
-
async function getReplicationTx(replicationStream:
|
|
586
|
-
let
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (
|
|
590
|
-
|
|
575
|
+
async function getReplicationTx(replicationStream: AsyncIterator<ChangeStreamBatch>, count: number): Promise<Buffer[]> {
|
|
576
|
+
let documents: Buffer[] = [];
|
|
577
|
+
while (true) {
|
|
578
|
+
const result = await replicationStream.next();
|
|
579
|
+
if (result.done) {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
const batch = result.value;
|
|
583
|
+
for (let buffer of batch.events) {
|
|
584
|
+
const doc = parseChangeDocument(buffer);
|
|
585
|
+
// Specifically filter out map_input / map_output collections
|
|
586
|
+
if (!doc?.ns?.coll?.startsWith('test_data')) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
documents.push((doc as any).fullDocument);
|
|
590
|
+
if (documents.length == count) {
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
591
593
|
}
|
|
592
|
-
|
|
593
|
-
if (transformed.length == count) {
|
|
594
|
+
if (documents.length == count) {
|
|
594
595
|
break;
|
|
595
596
|
}
|
|
596
597
|
}
|
|
597
|
-
|
|
598
|
-
|
|
598
|
+
replicationStream.return?.();
|
|
599
|
+
documents.sort((a, b) => Number(parseDocumentId(a).id) - Number(parseDocumentId(b).id));
|
|
600
|
+
return documents;
|
|
599
601
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseDocumentId } from '@module/replication/bufferToSqlite.js';
|
|
2
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
3
|
+
import { bson } from '@powersync/service-core';
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
|
|
6
|
+
function expectSameId(actual: any, expected: any) {
|
|
7
|
+
expect(actual).toEqual(expected);
|
|
8
|
+
expect(bson.serialize({ id: actual })).toEqual(bson.serialize({ id: expected }));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('parseDocumentId', () => {
|
|
12
|
+
test('matches naive bson.deserialize for representative _id values', () => {
|
|
13
|
+
const ids = [
|
|
14
|
+
1,
|
|
15
|
+
2n,
|
|
16
|
+
1 / Math.PI,
|
|
17
|
+
'abc',
|
|
18
|
+
{ a: 1, b: { c: 2 } },
|
|
19
|
+
new mongo.ObjectId('0123456789abcdef01234567'),
|
|
20
|
+
mongo.Timestamp.fromBits(123, 456),
|
|
21
|
+
new mongo.Binary(Buffer.from([1, 2, 3]), 0),
|
|
22
|
+
mongo.Decimal128.fromString('1234.5678'),
|
|
23
|
+
new mongo.MinKey(),
|
|
24
|
+
new mongo.MaxKey()
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
for (const id of ids) {
|
|
28
|
+
const source = bson.serialize({ before: 'x', _id: id, after: 1 }) as Buffer;
|
|
29
|
+
const parsed = parseDocumentId(source);
|
|
30
|
+
const naive = bson.deserialize(source, { useBigInt64: true })._id;
|
|
31
|
+
|
|
32
|
+
expectSameId(parsed.id, naive);
|
|
33
|
+
expect(parsed.idBuffer).toEqual(bson.serialize({ _id: naive }));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('finds _id regardless of field position', () => {
|
|
38
|
+
const id = { a: 1, b: 'two' };
|
|
39
|
+
const docs = [
|
|
40
|
+
{ _id: id, other: true },
|
|
41
|
+
{ other: true, _id: id },
|
|
42
|
+
{ first: 1, second: 'x', _id: id, third: false }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const doc of docs) {
|
|
46
|
+
const source = bson.serialize(doc) as Buffer;
|
|
47
|
+
const parsed = parseDocumentId(source);
|
|
48
|
+
const naive = bson.deserialize(source, { useBigInt64: true })._id;
|
|
49
|
+
|
|
50
|
+
expectSameId(parsed.id, naive);
|
|
51
|
+
expect(parsed.idBuffer).toEqual(bson.serialize({ _id: naive }));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|