@powersync/service-module-mongodb-storage 0.18.3 → 0.20.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 +53 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +13 -12
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +4 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +58 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +19 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +35 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +108 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +43 -2
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +9 -1
- package/dist/storage/implementation/v3/source-table-utils.js +20 -21
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +23 -22
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +20 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +76 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +53 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +40 -23
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +122 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +45 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +43 -35
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +124 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -16,7 +16,15 @@ import {
|
|
|
16
16
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
17
17
|
import { ParameterLookupRows, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
|
|
18
18
|
import * as bson from 'bson';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
clearCollectionInIdBatches,
|
|
21
|
+
clearCollectionInIdRanges,
|
|
22
|
+
idPrefixFilter,
|
|
23
|
+
mapOpEntry,
|
|
24
|
+
readSingleBatch,
|
|
25
|
+
retryOnMongoMaxTimeMSExpired,
|
|
26
|
+
setSessionSnapshotTime
|
|
27
|
+
} from '../../../utils/util.js';
|
|
20
28
|
import { MongoBucketStorage } from '../../MongoBucketStorage.js';
|
|
21
29
|
import { MongoSyncBucketStorageCheckpoint } from '../common/MongoSyncBucketStorageCheckpoint.js';
|
|
22
30
|
import { SourceKey } from '../models.js';
|
|
@@ -214,64 +222,59 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
protected async clearBucketData(signal?: AbortSignal): Promise<void> {
|
|
217
|
-
await
|
|
225
|
+
await clearCollectionInIdRanges(
|
|
226
|
+
this.logger,
|
|
218
227
|
'bucket data',
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
),
|
|
226
|
-
signal
|
|
228
|
+
this.db.bucket_data,
|
|
229
|
+
{
|
|
230
|
+
_id: idPrefixFilter<BucketDataKeyV1>({ g: this.replicationStreamId }, ['b', 'o'])
|
|
231
|
+
},
|
|
232
|
+
signal,
|
|
233
|
+
this.clearBatchThrottleRate
|
|
227
234
|
);
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
protected async clearParameterIndexes(signal?: AbortSignal): Promise<void> {
|
|
231
|
-
await
|
|
238
|
+
await clearCollectionInIdBatches(
|
|
239
|
+
this.logger,
|
|
232
240
|
'parameter index',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
),
|
|
240
|
-
signal
|
|
241
|
+
this.db.parameterIndexV1,
|
|
242
|
+
{
|
|
243
|
+
'key.g': this.replicationStreamId
|
|
244
|
+
},
|
|
245
|
+
signal,
|
|
246
|
+
this.clearBatchThrottleRate
|
|
241
247
|
);
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
protected async clearSourceRecords(signal?: AbortSignal): Promise<void> {
|
|
245
|
-
await
|
|
251
|
+
await clearCollectionInIdRanges(
|
|
252
|
+
this.logger,
|
|
246
253
|
'source records',
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
),
|
|
254
|
-
signal
|
|
254
|
+
this.db.sourceRecordsV1,
|
|
255
|
+
{
|
|
256
|
+
_id: idPrefixFilter<SourceKey>({ g: this.replicationStreamId }, ['t', 'k'])
|
|
257
|
+
},
|
|
258
|
+
signal,
|
|
259
|
+
this.clearBatchThrottleRate
|
|
255
260
|
);
|
|
256
261
|
}
|
|
257
262
|
|
|
258
263
|
protected async clearBucketState(signal?: AbortSignal): Promise<void> {
|
|
259
|
-
await
|
|
264
|
+
await clearCollectionInIdRanges(
|
|
265
|
+
this.logger,
|
|
260
266
|
'bucket state',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
),
|
|
268
|
-
signal
|
|
267
|
+
this.db.bucketStateV1,
|
|
268
|
+
{
|
|
269
|
+
_id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.replicationStreamId }, ['b'])
|
|
270
|
+
},
|
|
271
|
+
signal,
|
|
272
|
+
this.clearBatchThrottleRate
|
|
269
273
|
);
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
protected async clearSourceTables(signal?: AbortSignal): Promise<void> {
|
|
273
|
-
await
|
|
274
|
-
'source tables',
|
|
277
|
+
await retryOnMongoMaxTimeMSExpired(
|
|
275
278
|
() =>
|
|
276
279
|
this.db.sourceTablesV1(this.replicationStreamId).deleteMany(
|
|
277
280
|
{
|
|
@@ -279,7 +282,17 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
279
282
|
},
|
|
280
283
|
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
281
284
|
),
|
|
282
|
-
|
|
285
|
+
{
|
|
286
|
+
signal,
|
|
287
|
+
abortMessage: 'Aborted clearing data',
|
|
288
|
+
// This is a fairly long delay - only expected to hit this when the storage database is under high load.
|
|
289
|
+
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS,
|
|
290
|
+
onRetry: () => {
|
|
291
|
+
this.logger.info(
|
|
292
|
+
`Clearing batch of source tables timed out after ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, retrying...`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
283
296
|
);
|
|
284
297
|
}
|
|
285
298
|
|
|
@@ -1,58 +1,50 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { InternalOpId } from '@powersync/service-core';
|
|
3
3
|
import { BucketDataDoc, BucketKey } from '../common/BucketDataDoc.js';
|
|
4
|
-
import {
|
|
5
|
-
BucketDataDocumentGeneric,
|
|
6
|
-
BucketDataDocumentGenericId,
|
|
7
|
-
SingleBucketStore
|
|
8
|
-
} from '../common/SingleBucketStore.js';
|
|
9
4
|
import { BucketDataProperties } from '../models.js';
|
|
10
5
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
11
6
|
import { BucketDataDocumentV1, BucketDataKeyV1, serializeBucketDataV1 } from './models.js';
|
|
12
7
|
|
|
13
|
-
export class SingleBucketStoreV1
|
|
14
|
-
public readonly collection: mongo.Collection<
|
|
8
|
+
export class SingleBucketStoreV1 {
|
|
9
|
+
public readonly collection: mongo.Collection<BucketDataDocumentV1>;
|
|
15
10
|
|
|
16
11
|
constructor(
|
|
17
|
-
|
|
12
|
+
db: VersionedPowerSyncMongoV1,
|
|
18
13
|
public readonly key: BucketKey
|
|
19
14
|
) {
|
|
20
|
-
this.collection = db.bucketDataV1
|
|
15
|
+
this.collection = db.bucketDataV1;
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
docId(o: InternalOpId):
|
|
24
|
-
// `satisfies BucketDataKeyV1` checks that we use the correct type for V1 storage
|
|
25
|
-
// `as anyt` is to allow casting to the interface virtual type
|
|
18
|
+
docId(o: InternalOpId): BucketDataKeyV1 {
|
|
26
19
|
return {
|
|
27
20
|
g: this.key.replicationStreamId,
|
|
28
21
|
b: this.key.bucket,
|
|
29
22
|
o
|
|
30
|
-
}
|
|
23
|
+
};
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
get minId():
|
|
26
|
+
get minId(): BucketDataKeyV1 {
|
|
34
27
|
return {
|
|
35
28
|
g: this.key.replicationStreamId,
|
|
36
29
|
b: this.key.bucket,
|
|
37
|
-
o: new mongo.MinKey()
|
|
38
|
-
}
|
|
30
|
+
o: new mongo.MinKey() as any
|
|
31
|
+
};
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
get maxId():
|
|
34
|
+
get maxId(): BucketDataKeyV1 {
|
|
42
35
|
return {
|
|
43
36
|
g: this.key.replicationStreamId,
|
|
44
37
|
b: this.key.bucket,
|
|
45
|
-
o: new mongo.MaxKey()
|
|
46
|
-
}
|
|
38
|
+
o: new mongo.MaxKey() as any
|
|
39
|
+
};
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>):
|
|
50
|
-
return serializeBucketDataV1({ bucketKey: this.key, ...source })
|
|
42
|
+
toPersistedDocument(source: Omit<BucketDataDoc, 'bucketKey'>): BucketDataDocumentV1 {
|
|
43
|
+
return serializeBucketDataV1({ bucketKey: this.key, ...source });
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
fromPersistedDocument(doc:
|
|
54
|
-
const
|
|
55
|
-
const { _id, ...rest } = document;
|
|
46
|
+
fromPersistedDocument(doc: BucketDataDocumentV1): BucketDataDoc {
|
|
47
|
+
const { _id, ...rest } = doc;
|
|
56
48
|
return {
|
|
57
49
|
bucketKey: this.key,
|
|
58
50
|
o: _id.o,
|
|
@@ -61,10 +53,9 @@ export class SingleBucketStoreV1 implements SingleBucketStore {
|
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
fromPartialPersistedDocument<T extends keyof BucketDataProperties>(
|
|
64
|
-
doc: Pick<
|
|
56
|
+
doc: Pick<BucketDataDocumentV1, '_id' | T>
|
|
65
57
|
): Pick<BucketDataDoc, 'bucketKey' | 'o' | T> {
|
|
66
|
-
const
|
|
67
|
-
const { _id, ...rest } = document;
|
|
58
|
+
const { _id, ...rest } = doc;
|
|
68
59
|
return {
|
|
69
60
|
bucketKey: this.key,
|
|
70
61
|
o: _id.o,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { InternalOpId } from '@powersync/service-core';
|
|
3
|
+
import { BucketKey } from '../common/BucketDataDoc.js';
|
|
4
|
+
import { BucketDataKey } from '../models.js';
|
|
5
|
+
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
6
|
+
import { BucketDataDocumentV3 } from './models.js';
|
|
7
|
+
|
|
8
|
+
export class BucketDataContextV3 {
|
|
9
|
+
public readonly collection: mongo.Collection<BucketDataDocumentV3>;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
db: VersionedPowerSyncMongoV3,
|
|
13
|
+
public readonly key: BucketKey
|
|
14
|
+
) {
|
|
15
|
+
this.collection = db.bucketData(key.replicationStreamId, key.definitionId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
docId(o: InternalOpId): BucketDataKey {
|
|
19
|
+
return {
|
|
20
|
+
b: this.key.bucket,
|
|
21
|
+
o
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get minId(): BucketDataKey {
|
|
26
|
+
// MongoDB's MinKey sentinel does not match the bigint type for _id.o.
|
|
27
|
+
return {
|
|
28
|
+
b: this.key.bucket,
|
|
29
|
+
o: new mongo.MinKey()
|
|
30
|
+
} as unknown as BucketDataKey;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -43,7 +43,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
43
43
|
|
|
44
44
|
protected createPersistedBatch(writtenSize: number): PersistedBatch {
|
|
45
45
|
return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
|
|
46
|
-
logger: this.logger
|
|
46
|
+
logger: this.logger,
|
|
47
|
+
objectStorage: this.options.objectStorage,
|
|
48
|
+
inlineThresholdBytes: this.options.inlineThresholdBytes
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -146,23 +148,16 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
146
148
|
const mapping = parsedOverride?.mapping ?? this.mapping;
|
|
147
149
|
|
|
148
150
|
const { connection_id, source } = options;
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
type_oid: column.typeId
|
|
160
|
-
}))
|
|
161
|
-
},
|
|
162
|
-
storeCurrentData: source.sendsCompleteRows !== true,
|
|
163
|
-
syncConfig,
|
|
164
|
-
mapping,
|
|
165
|
-
desired: sourceTableDesiredResolution(syncConfig, source, mapping)
|
|
151
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
152
|
+
const identity = {
|
|
153
|
+
schema: source.schema,
|
|
154
|
+
name: source.name,
|
|
155
|
+
objectId: source.objectId,
|
|
156
|
+
replicaIdColumns: source.replicaIdColumns.map((column) => ({
|
|
157
|
+
name: column.name,
|
|
158
|
+
type: column.type,
|
|
159
|
+
type_oid: column.typeId
|
|
160
|
+
}))
|
|
166
161
|
};
|
|
167
162
|
|
|
168
163
|
let result: storage.ResolveTablesResult | null = null;
|
|
@@ -172,14 +167,36 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
172
167
|
await session.withTransaction(async () => {
|
|
173
168
|
const col = this.db.sourceTables(this.replicationStreamId);
|
|
174
169
|
|
|
175
|
-
//
|
|
176
|
-
// Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
|
|
170
|
+
// Find records that overlap by name or relation id.
|
|
177
171
|
const candidateDocs = await col
|
|
178
|
-
.find(overlappingSourceTableFilter(connection_id,
|
|
172
|
+
.find(overlappingSourceTableFilter(connection_id, identity), { session })
|
|
179
173
|
.toArray();
|
|
180
174
|
|
|
181
|
-
|
|
182
|
-
|
|
175
|
+
const candidateTables = candidateDocs.map((doc) =>
|
|
176
|
+
sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping)
|
|
177
|
+
);
|
|
178
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
179
|
+
const resolution = await reconcile({ source, candidates });
|
|
180
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
181
|
+
|
|
182
|
+
// Persist metadata from the reconciler without mutating the queried documents.
|
|
183
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
184
|
+
await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const context: SourceTableReconciliationContext = {
|
|
188
|
+
connectionId: connection_id,
|
|
189
|
+
connectionTag: source.connectionTag,
|
|
190
|
+
identity,
|
|
191
|
+
storeCurrentData: source.sendsCompleteRows !== true,
|
|
192
|
+
syncConfig,
|
|
193
|
+
mapping,
|
|
194
|
+
desired: sourceTableDesiredResolution(syncConfig, source, mapping),
|
|
195
|
+
sourceCompatibleTables: resolution.compatibleTables,
|
|
196
|
+
newTableSourceMetadata: resolution.newTableValues.sourceMetadata
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Plan record reuse, membership changes, creation, and removal.
|
|
183
200
|
const plan = planSourceTableReconciliation(candidateDocs, context);
|
|
184
201
|
|
|
185
202
|
// Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
3
|
import {
|
|
3
4
|
bson,
|
|
4
5
|
BucketChecksum,
|
|
6
|
+
CheckpointChecksumInvalidatedError,
|
|
5
7
|
FetchPartialBucketChecksum,
|
|
6
8
|
InternalOpId,
|
|
7
9
|
isPartialChecksum,
|
|
@@ -179,9 +181,42 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
179
181
|
requests.set(request.bucket, request);
|
|
180
182
|
}
|
|
181
183
|
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
184
|
+
const aggregate = await this.aggregatePartialChecksums(collection, requests, context);
|
|
185
|
+
const { checksums, startStraddledBuckets } = this.normalizePartialChecksumResults(batch, aggregate);
|
|
186
|
+
|
|
187
|
+
if (startStraddledBuckets.size == 0) {
|
|
188
|
+
return checksums;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A cached base ends inside a compaction-produced document. Its checksum cannot
|
|
192
|
+
// be safely combined with the document-level checksum, so recalculate this
|
|
193
|
+
// bucket from the beginning. A full result replaces the cached base upstream.
|
|
194
|
+
const fullRequests = batch
|
|
195
|
+
.filter((request) => startStraddledBuckets.has(request.bucket))
|
|
196
|
+
.map((request) => ({ ...request, start: undefined }));
|
|
197
|
+
const fullRequestMap = new Map(fullRequests.map((request) => [request.bucket, request]));
|
|
198
|
+
const fullAggregate = await this.aggregatePartialChecksums(collection, fullRequestMap, context);
|
|
199
|
+
const fullResults = this.normalizePartialChecksumResults(fullRequests, fullAggregate);
|
|
200
|
+
|
|
201
|
+
if (fullResults.startStraddledBuckets.size > 0) {
|
|
202
|
+
throw new ServiceAssertionError(
|
|
203
|
+
`Unexpected start-boundary straddle while recalculating bucket(s) ${[...fullResults.startStraddledBuckets].join(', ')}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
for (const [bucket, checksum] of fullResults.checksums) {
|
|
207
|
+
checksums.set(bucket, checksum);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return checksums;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async aggregatePartialChecksums(
|
|
214
|
+
collection: lib_mongo.mongo.Collection<BucketDataDocumentV3>,
|
|
215
|
+
requests: Map<string, FetchPartialBucketChecksumByBucket>,
|
|
216
|
+
context: MongoChecksumSessionContext
|
|
217
|
+
): Promise<bson.Document[]> {
|
|
218
|
+
return collection
|
|
219
|
+
.aggregate(this.buildPartialChecksumPipeline(requests), {
|
|
185
220
|
...context.readOptions,
|
|
186
221
|
maxTimeMS: lib_mongo.MONGO_CHECKSUM_TIMEOUT_MS
|
|
187
222
|
})
|
|
@@ -189,8 +224,6 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
189
224
|
.catch((e) => {
|
|
190
225
|
throw lib_mongo.mapQueryError(e, 'while reading checksums');
|
|
191
226
|
});
|
|
192
|
-
|
|
193
|
-
return this.normalizePartialChecksumResults(batch, aggregate);
|
|
194
227
|
}
|
|
195
228
|
|
|
196
229
|
private buildPartialChecksumPipeline(requests: Map<string, FetchPartialBucketChecksumByBucket>): bson.Document[] {
|
|
@@ -203,128 +236,64 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
203
236
|
$or: filters
|
|
204
237
|
}
|
|
205
238
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
bucket_start: {
|
|
209
|
-
$switch: {
|
|
210
|
-
branches: Array.from(requests.entries()).map(([bucket, req]) => ({
|
|
211
|
-
case: { $eq: ['$_id.b', bucket] },
|
|
212
|
-
then: req.start ?? new bson.MinKey()
|
|
213
|
-
})),
|
|
214
|
-
default: new bson.MinKey()
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
bucket_end: {
|
|
218
|
-
$switch: {
|
|
219
|
-
branches: Array.from(requests.entries()).map(([bucket, req]) => ({
|
|
220
|
-
case: { $eq: ['$_id.b', bucket] },
|
|
221
|
-
then: req.end
|
|
222
|
-
})),
|
|
223
|
-
default: new bson.MaxKey()
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
// Determine if document is fully included within [start, end] range
|
|
239
|
+
// Only document-level metadata is used below. Bucket-data operations may
|
|
240
|
+
// be offloaded, and checksum queries must never inspect the ops array.
|
|
229
241
|
{
|
|
230
242
|
$project: {
|
|
231
243
|
_id: 1,
|
|
232
244
|
min_op: 1,
|
|
233
245
|
checksum: 1,
|
|
234
246
|
count: 1,
|
|
235
|
-
|
|
236
|
-
bucket_start: 1,
|
|
237
|
-
bucket_end: 1,
|
|
238
|
-
is_fully_included: {
|
|
239
|
-
$and: [{ $gt: ['$min_op', '$bucket_start'] }, { $lte: ['$_id.o', '$bucket_end'] }]
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
// Compute included checksum, count, and clear op detection
|
|
244
|
-
{
|
|
245
|
-
$project: {
|
|
246
|
-
_id: 1,
|
|
247
|
-
checksum_total: {
|
|
248
|
-
$cond: {
|
|
249
|
-
if: '$is_fully_included',
|
|
250
|
-
then: '$checksum',
|
|
251
|
-
else: {
|
|
252
|
-
$sum: {
|
|
253
|
-
$map: {
|
|
254
|
-
input: {
|
|
255
|
-
$filter: {
|
|
256
|
-
input: '$ops',
|
|
257
|
-
cond: {
|
|
258
|
-
$and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
in: '$$this.checksum'
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
count_total: {
|
|
269
|
-
$cond: {
|
|
270
|
-
if: '$is_fully_included',
|
|
271
|
-
then: '$count',
|
|
272
|
-
else: {
|
|
273
|
-
$size: {
|
|
274
|
-
$filter: {
|
|
275
|
-
input: '$ops',
|
|
276
|
-
cond: {
|
|
277
|
-
$and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
has_clear_op: {
|
|
285
|
-
$max: {
|
|
286
|
-
$map: {
|
|
287
|
-
input: {
|
|
288
|
-
$filter: {
|
|
289
|
-
input: '$ops',
|
|
290
|
-
cond: {
|
|
291
|
-
$and: [{ $gt: ['$$this.o', '$bucket_start'] }, { $lte: ['$$this.o', '$bucket_end'] }]
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
in: { $cond: [{ $eq: ['$$this.op', 'CLEAR'] }, 1, 0] }
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
},
|
|
299
|
-
last_op: { $max: '$_id.o' }
|
|
247
|
+
has_clear_op: 1
|
|
300
248
|
}
|
|
301
249
|
},
|
|
302
|
-
//
|
|
250
|
+
// Aggregate document metadata per bucket. A CLEAR makes the result a full
|
|
251
|
+
// checksum that replaces any cached prefix; storage guarantees that
|
|
252
|
+
// documents preceding the CLEAR have already been removed.
|
|
303
253
|
{
|
|
304
254
|
$group: {
|
|
305
255
|
_id: '$_id.b',
|
|
306
|
-
checksum_total: { $sum: '$
|
|
307
|
-
count: { $sum: '$
|
|
308
|
-
has_clear_op: {
|
|
309
|
-
|
|
256
|
+
checksum_total: { $sum: '$checksum' },
|
|
257
|
+
count: { $sum: '$count' },
|
|
258
|
+
has_clear_op: {
|
|
259
|
+
$max: { $cond: ['$has_clear_op', 1, 0] }
|
|
260
|
+
},
|
|
261
|
+
min_op: { $min: '$min_op' },
|
|
262
|
+
max_op: { $max: '$_id.o' }
|
|
310
263
|
}
|
|
311
|
-
}
|
|
312
|
-
// $sort results
|
|
313
|
-
{ $sort: { _id: 1 } }
|
|
264
|
+
}
|
|
314
265
|
];
|
|
315
266
|
}
|
|
316
267
|
|
|
317
268
|
private normalizePartialChecksumResults(
|
|
318
269
|
batch: FetchPartialBucketChecksumByBucket[],
|
|
319
270
|
aggregate: bson.Document[]
|
|
320
|
-
): PartialChecksumMap {
|
|
271
|
+
): { checksums: PartialChecksumMap; startStraddledBuckets: Set<string> } {
|
|
272
|
+
const requests = new Map(batch.map((request) => [request.bucket, request]));
|
|
273
|
+
const startStraddledBuckets = new Set<string>();
|
|
321
274
|
const partialChecksums = new Map<string, PartialOrFullChecksum>();
|
|
322
|
-
|
|
323
|
-
|
|
275
|
+
|
|
276
|
+
for (const doc of aggregate) {
|
|
277
|
+
const bucket = doc._id as string;
|
|
278
|
+
const request = requests.get(bucket)!;
|
|
279
|
+
|
|
280
|
+
// A maximum beyond end means a matched document straddles the checkpoint.
|
|
281
|
+
// Its document-level checksum includes later operations, so this checkpoint
|
|
282
|
+
// cannot be calculated from document metadata.
|
|
283
|
+
if (doc.max_op > request.end) {
|
|
284
|
+
throw new CheckpointChecksumInvalidatedError(request.end, bucket);
|
|
285
|
+
}
|
|
286
|
+
// The _id filter excludes documents ending at or before start. A minimum
|
|
287
|
+
// at or below start therefore means a matched document straddles it.
|
|
288
|
+
if (request.start != null && doc.min_op <= request.start) {
|
|
289
|
+
startStraddledBuckets.add(bucket);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
324
293
|
partialChecksums.set(bucket, checksumFromAggregate(doc));
|
|
325
294
|
}
|
|
326
295
|
|
|
327
|
-
|
|
296
|
+
const checksums = new Map<string, PartialOrFullChecksum>(
|
|
328
297
|
batch.map((request) => {
|
|
329
298
|
const bucket = request.bucket;
|
|
330
299
|
let partialChecksum = partialChecksums.get(bucket);
|
|
@@ -346,6 +315,7 @@ export class MongoChecksumsV3 extends MongoChecksums implements DefinitionChecks
|
|
|
346
315
|
return [bucket, partialChecksum];
|
|
347
316
|
})
|
|
348
317
|
);
|
|
318
|
+
return { checksums, startStraddledBuckets };
|
|
349
319
|
}
|
|
350
320
|
}
|
|
351
321
|
|