@powersync/service-module-mongodb-storage 0.17.0 → 0.18.1
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 +27 -0
- package/dist/storage/MongoBucketStorage.d.ts +34 -16
- package/dist/storage/MongoBucketStorage.js +219 -70
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +42 -23
- package/dist/storage/implementation/MongoBucketBatch.js +61 -44
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +10 -9
- package/dist/storage/implementation/MongoChecksums.js +3 -111
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
- package/dist/storage/implementation/MongoCompactor.js +18 -9
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
- package/dist/storage/implementation/MongoHydrationState.js +49 -0
- package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
- package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +5 -2
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +41 -23
- package/dist/storage/implementation/MongoSyncBucketStorage.js +51 -43
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +1 -2
- package/dist/storage/implementation/common/PersistedBatch.js +5 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.js +9 -14
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +4 -13
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +42 -26
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +15 -17
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +47 -45
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.d.ts +10 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +286 -259
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +19 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +40 -28
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +232 -116
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +13 -12
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +29 -27
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
- package/dist/storage/implementation/v3/bucket-format.js +58 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
- package/dist/storage/implementation/v3/chunking.d.ts +10 -0
- package/dist/storage/implementation/v3/chunking.js +28 -0
- package/dist/storage/implementation/v3/chunking.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +64 -21
- package/dist/storage/implementation/v3/models.js +11 -28
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
- package/dist/storage/implementation/v3/source-table-utils.js +214 -0
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
- package/dist/storage/storage-index.d.ts +4 -4
- package/dist/storage/storage-index.js +4 -4
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +2 -2
- package/dist/utils/test-utils.d.ts +1 -2
- package/dist/utils/test-utils.js +5 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +1 -1
- package/dist/utils/util.js +3 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -19
- package/src/storage/MongoBucketStorage.ts +334 -87
- package/src/storage/implementation/MongoBucketBatch.ts +86 -58
- package/src/storage/implementation/MongoChecksums.ts +6 -138
- package/src/storage/implementation/MongoCompactor.ts +19 -9
- package/src/storage/implementation/MongoHydrationState.ts +61 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
- package/src/storage/implementation/MongoStorageProvider.ts +5 -2
- package/src/storage/implementation/MongoSyncBucketStorage.ts +70 -56
- package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
- package/src/storage/implementation/common/PersistedBatch.ts +7 -3
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
- package/src/storage/implementation/db.ts +11 -19
- package/src/storage/implementation/models.ts +4 -14
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +55 -34
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +70 -64
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
- package/src/storage/implementation/v1/models.ts +11 -1
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +358 -314
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
- package/src/storage/implementation/v3/MongoCompactorV3.ts +646 -29
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +315 -157
- package/src/storage/implementation/v3/PersistedBatchV3.ts +62 -36
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +41 -36
- package/src/storage/implementation/v3/bucket-format.ts +70 -0
- package/src/storage/implementation/v3/chunking.ts +38 -0
- package/src/storage/implementation/v3/models.ts +93 -55
- package/src/storage/implementation/v3/source-table-utils.ts +392 -0
- package/src/storage/storage-index.ts +12 -4
- package/src/utils/test-utils.ts +6 -3
- package/src/utils/util.ts +3 -2
- package/test/src/__snapshots__/storage.test.ts.snap +0 -75
- package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
- package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
- package/test/src/storage.test.ts +11 -11
- package/test/src/storage_compacting.test.ts +1956 -6
- package/test/src/storage_sync.test.ts +1077 -68
- package/test/src/storeCurrentData.test.ts +17 -7
- package/test/src/util.ts +2 -1
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
- package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
- package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
- package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
- package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MongoSyncBucketStorageV3 } from '@module/storage/implementation/v3/MongoSyncBucketStorageV3.js';
|
|
2
|
+
import {
|
|
3
|
+
deserializeParameterLookup,
|
|
4
|
+
JwtPayload,
|
|
5
|
+
SingleSyncConfigBucketDefinitionMapping,
|
|
6
|
+
storage,
|
|
7
|
+
updateSyncRulesFromYaml
|
|
8
|
+
} from '@powersync/service-core';
|
|
2
9
|
import { bucketRequest, register, test_utils } from '@powersync/service-core-tests';
|
|
3
|
-
import {
|
|
10
|
+
import { RequestParameters, ScopedParameterLookup, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
4
11
|
import * as bson from 'bson';
|
|
5
|
-
import * as sqlite from 'node:sqlite';
|
|
6
12
|
import { describe, expect, test } from 'vitest';
|
|
7
13
|
import { MongoBucketStorage } from '../../src/storage/MongoBucketStorage.js';
|
|
14
|
+
import { MongoParsedSyncConfigSet } from '../../src/storage/implementation/MongoParsedSyncConfigSet.js';
|
|
15
|
+
import { MongoPersistedSyncConfigContentV3 } from '../../src/storage/implementation/MongoPersistedSyncConfigContent.js';
|
|
16
|
+
import { BucketDataDoc, BucketKey } from '../../src/storage/implementation/common/BucketDataDoc.js';
|
|
8
17
|
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
18
|
+
import { getMongoStorageConfig } from '../../src/storage/implementation/models.js';
|
|
9
19
|
import { SourceRecordStoreV3 } from '../../src/storage/implementation/v3/SourceRecordStoreV3.js';
|
|
10
20
|
import type { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
21
|
+
import { serializeBucketData } from '../../src/storage/implementation/v3/bucket-format.js';
|
|
11
22
|
import {
|
|
12
23
|
CurrentBucketV3,
|
|
13
24
|
ReplicationStreamDocumentV3,
|
|
@@ -53,10 +64,54 @@ function objectIdGenerator(id: string) {
|
|
|
53
64
|
};
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Build a standalone parsed sync config set for the given yaml, with its own mapping.
|
|
69
|
+
*
|
|
70
|
+
* This keeps the sync rules and the bucket definition mapping paired, as required by
|
|
71
|
+
* `ResolveTablesOptions.parsedSyncConfig`.
|
|
72
|
+
*/
|
|
73
|
+
function parsedSyncConfigSetFor(yaml: string, storageVersion: number) {
|
|
57
74
|
const parsed = SqlSyncRules.fromYaml(yaml, test_utils.PARSE_OPTIONS);
|
|
58
75
|
expect(parsed.errors).toEqual([]);
|
|
59
|
-
return
|
|
76
|
+
return new MongoParsedSyncConfigSet(1, getMongoStorageConfig(storageVersion), 'test_slot', [
|
|
77
|
+
{
|
|
78
|
+
syncConfigId: new bson.ObjectId().toHexString(),
|
|
79
|
+
syncConfig: parsed,
|
|
80
|
+
mapping: SingleSyncConfigBucketDefinitionMapping.fromParsedSyncConfig(parsed)
|
|
81
|
+
}
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function getMongoSyncConfigContents(factory: storage.BucketStorageFactory, replicationStreamId: number) {
|
|
86
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
87
|
+
const doc = (await mongoFactory.db.sync_rules.findOne({
|
|
88
|
+
_id: replicationStreamId
|
|
89
|
+
})) as ReplicationStreamDocumentV3 | null;
|
|
90
|
+
if (doc == null) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const db = mongoFactory.db.versioned(getMongoStorageConfig(doc.storage_version)) as VersionedPowerSyncMongoV3;
|
|
95
|
+
const syncConfigDocs = await db.syncConfigDefinitions
|
|
96
|
+
.find({
|
|
97
|
+
_id: { $in: doc.sync_configs.map((config) => config._id) }
|
|
98
|
+
})
|
|
99
|
+
.toArray();
|
|
100
|
+
|
|
101
|
+
return syncConfigDocs.map((config) => new MongoPersistedSyncConfigContentV3(mongoFactory.db, doc, config));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get a MongoDB V3 storage bucket definition id, from a bucket name.
|
|
106
|
+
*
|
|
107
|
+
* This relies on internals of bucket naming, but helps for simplifying tests.
|
|
108
|
+
*/
|
|
109
|
+
function bucketDefinitionId(bucket: string) {
|
|
110
|
+
const match = bucket.match(/^\w+\|\w+\.\w+\.(\w+)\[/);
|
|
111
|
+
if (match == null) {
|
|
112
|
+
throw new Error(`Expected versioned bucket name, got ${bucket}`);
|
|
113
|
+
}
|
|
114
|
+
return parseInt(match[1], 16);
|
|
60
115
|
}
|
|
61
116
|
|
|
62
117
|
function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, storageVersion: number) {
|
|
@@ -82,6 +137,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
82
137
|
)
|
|
83
138
|
);
|
|
84
139
|
const bucketStorage = factory.getInstance(syncRules);
|
|
140
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
85
141
|
|
|
86
142
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
87
143
|
|
|
@@ -136,7 +192,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
136
192
|
|
|
137
193
|
const options: storage.BucketDataBatchOptions = {};
|
|
138
194
|
const batch1 = await test_utils.fromAsync(
|
|
139
|
-
bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(
|
|
195
|
+
bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRulesContent, 'global[]', 0n)], options)
|
|
140
196
|
);
|
|
141
197
|
expect(test_utils.getBatchData(batch1)).toEqual([
|
|
142
198
|
{ op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
|
|
@@ -151,7 +207,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
151
207
|
const batch2 = await test_utils.fromAsync(
|
|
152
208
|
bucketStorage.getBucketDataBatch(
|
|
153
209
|
checkpoint,
|
|
154
|
-
[bucketRequest(
|
|
210
|
+
[bucketRequest(syncRulesContent, 'global[]', batch1[0].chunkData.next_after)],
|
|
155
211
|
options
|
|
156
212
|
)
|
|
157
213
|
);
|
|
@@ -167,7 +223,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
167
223
|
const batch3 = await test_utils.fromAsync(
|
|
168
224
|
bucketStorage.getBucketDataBatch(
|
|
169
225
|
checkpoint,
|
|
170
|
-
[bucketRequest(
|
|
226
|
+
[bucketRequest(syncRulesContent, 'global[]', batch2[0].chunkData.next_after)],
|
|
171
227
|
options
|
|
172
228
|
)
|
|
173
229
|
);
|
|
@@ -189,7 +245,10 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
189
245
|
await Promise.all(
|
|
190
246
|
(
|
|
191
247
|
await mongoFactory.db.db
|
|
192
|
-
.listCollections(
|
|
248
|
+
.listCollections(
|
|
249
|
+
{ name: new RegExp(`^bucket_data_${syncRules.replicationStreamId}_`) },
|
|
250
|
+
{ nameOnly: true }
|
|
251
|
+
)
|
|
193
252
|
.toArray()
|
|
194
253
|
).map((collection: { name: string }) =>
|
|
195
254
|
mongoFactory.db.db
|
|
@@ -388,8 +447,8 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
388
447
|
}
|
|
389
448
|
);
|
|
390
449
|
|
|
391
|
-
test.runIf(storageVersion >= 3)('resolveTables handles v3 source membership additions
|
|
392
|
-
// Tests the behavior of resolveTables when bucket data sources and parameter index creators are added
|
|
450
|
+
test.runIf(storageVersion >= 3)('resolveTables handles v3 source membership additions', async () => {
|
|
451
|
+
// Tests the behavior of resolveTables when bucket data sources and parameter index creators are added.
|
|
393
452
|
// These are not end-to-end tests yet, since we don't have a full incremental reprocessing implementation.
|
|
394
453
|
// This just tests the specific resolveTables behavior.
|
|
395
454
|
|
|
@@ -434,20 +493,19 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
434
493
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(fullRulesYaml, { storageVersion }));
|
|
435
494
|
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
436
495
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
437
|
-
const fullRules =
|
|
438
|
-
const dataOnlyRules =
|
|
439
|
-
const parameterOnlyRules =
|
|
440
|
-
const eventOnlyRules =
|
|
496
|
+
const fullRules = parsedSyncConfigSetFor(fullRulesYaml, storageVersion);
|
|
497
|
+
const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyRulesYaml, storageVersion);
|
|
498
|
+
const parameterOnlyRules = parsedSyncConfigSetFor(parameterOnlyRulesYaml, storageVersion);
|
|
499
|
+
const eventOnlyRules = parsedSyncConfigSetFor(eventOnlyRulesYaml, storageVersion);
|
|
441
500
|
const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
|
|
442
501
|
const dataOnlyTableId = new bson.ObjectId('6544e3899293153fa7b38348');
|
|
443
502
|
const addedParameterTableId = new bson.ObjectId('6544e3899293153fa7b38349');
|
|
444
|
-
const removedDataTableId = new bson.ObjectId('6544e3899293153fa7b3834a');
|
|
445
503
|
|
|
446
504
|
const dataOnly = await writer.resolveTables({
|
|
447
505
|
connection_id: 1,
|
|
448
506
|
source,
|
|
449
507
|
idGenerator: () => dataOnlyTableId,
|
|
450
|
-
|
|
508
|
+
parsedSyncConfig: dataOnlyRules
|
|
451
509
|
});
|
|
452
510
|
expect(dataOnly.tables.map((table) => table.id)).toEqual([dataOnlyTableId]);
|
|
453
511
|
expect(dataOnly.dropTables.map((table) => table.id)).toEqual([]);
|
|
@@ -458,7 +516,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
458
516
|
connection_id: 1,
|
|
459
517
|
source,
|
|
460
518
|
idGenerator: () => addedParameterTableId,
|
|
461
|
-
|
|
519
|
+
parsedSyncConfig: fullRules
|
|
462
520
|
});
|
|
463
521
|
// Adding a definition always creates a new SourceTable
|
|
464
522
|
expect(addedParameter.tables.map((table) => table.id)).toEqual([dataOnlyTableId, addedParameterTableId]);
|
|
@@ -472,29 +530,29 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
472
530
|
idGenerator: () => {
|
|
473
531
|
throw new Error('data-only resolve should reuse existing v3 source table');
|
|
474
532
|
},
|
|
475
|
-
|
|
533
|
+
parsedSyncConfig: dataOnlyRules
|
|
476
534
|
});
|
|
477
535
|
expect(removedParameter.tables.map((table) => table.id)).toEqual([dataOnlyTableId]);
|
|
478
|
-
//
|
|
479
|
-
expect(removedParameter.dropTables.map((table) => table.id)).toEqual([
|
|
536
|
+
// Same-identity tables that no longer cover desired memberships are left in place.
|
|
537
|
+
expect(removedParameter.dropTables.map((table) => table.id)).toEqual([]);
|
|
480
538
|
expect(removedParameter.tables[0].bucketDataSources).toHaveLength(1);
|
|
481
539
|
expect(removedParameter.tables[0].parameterLookupSources).toHaveLength(0);
|
|
482
|
-
await writer.drop(removedParameter.dropTables);
|
|
483
540
|
|
|
484
541
|
const removedData = await writer.resolveTables({
|
|
485
542
|
connection_id: 1,
|
|
486
543
|
source,
|
|
487
|
-
idGenerator: () =>
|
|
488
|
-
|
|
544
|
+
idGenerator: () => {
|
|
545
|
+
throw new Error('parameter-only resolve should reuse existing v3 source table');
|
|
546
|
+
},
|
|
547
|
+
parsedSyncConfig: parameterOnlyRules
|
|
489
548
|
});
|
|
490
549
|
|
|
491
|
-
// This goes from dataOnlyRules -> parameterOnlyRules, which
|
|
492
|
-
//
|
|
493
|
-
expect(removedData.tables.map((table) => table.id)).toEqual([
|
|
494
|
-
expect(removedData.dropTables.map((table) => table.id)).toEqual([
|
|
550
|
+
// This goes from dataOnlyRules -> parameterOnlyRules, which removes one definition but
|
|
551
|
+
// reuses the existing parameter-only source table.
|
|
552
|
+
expect(removedData.tables.map((table) => table.id)).toEqual([addedParameterTableId]);
|
|
553
|
+
expect(removedData.dropTables.map((table) => table.id)).toEqual([]);
|
|
495
554
|
expect(removedData.tables[0].bucketDataSources).toHaveLength(0);
|
|
496
555
|
expect(removedData.tables[0].parameterLookupSources).toHaveLength(1);
|
|
497
|
-
await writer.drop(removedData.dropTables);
|
|
498
556
|
|
|
499
557
|
const eventOnly = await writer.resolveTables({
|
|
500
558
|
connection_id: 1,
|
|
@@ -502,11 +560,12 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
502
560
|
idGenerator: () => {
|
|
503
561
|
throw new Error('resolve should reuse existing v3 source table');
|
|
504
562
|
},
|
|
505
|
-
|
|
563
|
+
parsedSyncConfig: eventOnlyRules
|
|
506
564
|
});
|
|
507
565
|
|
|
508
566
|
// Event-only table can re-use any existing table.
|
|
509
|
-
expect(eventOnly.tables
|
|
567
|
+
expect(eventOnly.tables).toHaveLength(1);
|
|
568
|
+
expect([dataOnlyTableId.toString(), addedParameterTableId.toString()]).toContain(eventOnly.tables[0].id.toString());
|
|
510
569
|
expect(eventOnly.dropTables.map((table) => table.id)).toEqual([]);
|
|
511
570
|
expect(eventOnly.tables[0].bucketDataSources).toHaveLength(0);
|
|
512
571
|
expect(eventOnly.tables[0].parameterLookupSources).toHaveLength(0);
|
|
@@ -515,6 +574,76 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
515
574
|
expect(eventOnly.tables[0].syncEvent).toBe(true);
|
|
516
575
|
});
|
|
517
576
|
|
|
577
|
+
test.runIf(storageVersion >= 3)(
|
|
578
|
+
'resolveTables designates a single event carrier across split source tables',
|
|
579
|
+
async () => {
|
|
580
|
+
// When memberships are split over multiple SourceTables for the same ref, a row change
|
|
581
|
+
// is saved once per table. Only one table may fire events, otherwise the same event
|
|
582
|
+
// would fire once per table for every row change.
|
|
583
|
+
const dataOnlyEventYaml = `
|
|
584
|
+
bucket_definitions:
|
|
585
|
+
by_owner:
|
|
586
|
+
parameters:
|
|
587
|
+
- SELECT token_parameters.owner_id as owner_id
|
|
588
|
+
data:
|
|
589
|
+
- SELECT id, owner_id FROM memberships WHERE owner_id = bucket.owner_id
|
|
590
|
+
|
|
591
|
+
event_definitions:
|
|
592
|
+
write_checkpoints:
|
|
593
|
+
payloads:
|
|
594
|
+
- SELECT id, owner_id FROM memberships
|
|
595
|
+
`;
|
|
596
|
+
const fullEventYaml = `
|
|
597
|
+
bucket_definitions:
|
|
598
|
+
by_owner:
|
|
599
|
+
parameters:
|
|
600
|
+
- SELECT owner_id FROM memberships WHERE id = token_parameters.test_id
|
|
601
|
+
data:
|
|
602
|
+
- SELECT id, owner_id FROM memberships WHERE owner_id = bucket.owner_id
|
|
603
|
+
|
|
604
|
+
event_definitions:
|
|
605
|
+
write_checkpoints:
|
|
606
|
+
payloads:
|
|
607
|
+
- SELECT id, owner_id FROM memberships
|
|
608
|
+
`;
|
|
609
|
+
|
|
610
|
+
await using factory = await storageConfig.factory();
|
|
611
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(dataOnlyEventYaml, { storageVersion }));
|
|
612
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
613
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
614
|
+
const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyEventYaml, storageVersion);
|
|
615
|
+
const fullRules = parsedSyncConfigSetFor(fullEventYaml, storageVersion);
|
|
616
|
+
const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
|
|
617
|
+
|
|
618
|
+
const initial = await writer.resolveTables({
|
|
619
|
+
connection_id: 1,
|
|
620
|
+
source,
|
|
621
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834b'),
|
|
622
|
+
parsedSyncConfig: dataOnlyRules
|
|
623
|
+
});
|
|
624
|
+
expect(initial.tables).toHaveLength(1);
|
|
625
|
+
expect(initial.tables[0].syncEvent).toBe(true);
|
|
626
|
+
|
|
627
|
+
// Adding the table-based parameter lookup creates a second SourceTable for the same ref.
|
|
628
|
+
const split = await writer.resolveTables({
|
|
629
|
+
connection_id: 1,
|
|
630
|
+
source,
|
|
631
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834c'),
|
|
632
|
+
parsedSyncConfig: fullRules
|
|
633
|
+
});
|
|
634
|
+
expect(split.tables).toHaveLength(2);
|
|
635
|
+
// Both tables match the event by ref, but only one may carry it.
|
|
636
|
+
const carriers = split.tables.filter((table) => table.syncEvent);
|
|
637
|
+
expect(carriers).toHaveLength(1);
|
|
638
|
+
|
|
639
|
+
// getSourceTableStatus preserves the carrier designation rather than recomputing it
|
|
640
|
+
// from the ref, so refreshing a non-carrier table does not make it fire events.
|
|
641
|
+
const nonCarrier = split.tables.find((table) => !table.syncEvent)!;
|
|
642
|
+
const refreshed = await writer.getSourceTableStatus(nonCarrier);
|
|
643
|
+
expect(refreshed!.syncEvent).toBe(false);
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
|
|
518
647
|
test.runIf(storageVersion >= 3)('uses v3 mongodb model shapes', async () => {
|
|
519
648
|
await using factory = await storageConfig.factory();
|
|
520
649
|
const syncRules = await factory.updateSyncRules(
|
|
@@ -531,7 +660,8 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
531
660
|
)
|
|
532
661
|
);
|
|
533
662
|
const bucketStorage = factory.getInstance(syncRules);
|
|
534
|
-
const
|
|
663
|
+
const syncRulesContent = syncRules.syncConfigContent[0];
|
|
664
|
+
const sync_rules = syncRulesContent.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig;
|
|
535
665
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
536
666
|
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
537
667
|
|
|
@@ -552,7 +682,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
552
682
|
const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: { test: 'shape-check' } }), {});
|
|
553
683
|
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
554
684
|
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
555
|
-
async getParameterSets(lookups) {
|
|
685
|
+
async getParameterSets(lookups: ScopedParameterLookup[]) {
|
|
556
686
|
expect(lookups.map((l) => l.indexKey)).toEqual([['shape-check']]);
|
|
557
687
|
expect(lookups[0].indexId).toEqual('1');
|
|
558
688
|
|
|
@@ -561,33 +691,635 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
561
691
|
return parameter_sets;
|
|
562
692
|
}
|
|
563
693
|
});
|
|
564
|
-
expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(
|
|
694
|
+
expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRulesContent, 'global["user-1"]').bucket]);
|
|
565
695
|
|
|
566
696
|
const mongoFactory = factory as MongoBucketStorage;
|
|
567
697
|
const db = (bucketStorage as MongoSyncBucketStorage).db as VersionedPowerSyncMongoV3;
|
|
568
|
-
const currentDataCollections = await db.
|
|
698
|
+
const currentDataCollections = await db.listSourceRecordCollections(syncRules.replicationStreamId);
|
|
569
699
|
const currentData = await currentDataCollections[0]?.findOne({});
|
|
570
700
|
const firstBucket: CurrentBucketV3 | undefined = currentData?.buckets[0] as CurrentBucketV3 | undefined;
|
|
571
701
|
expect(firstBucket?.def).toMatch(/^[0-9a-f]+$/);
|
|
572
702
|
|
|
573
703
|
const bucketCollections = await mongoFactory.db.db
|
|
574
|
-
.listCollections({ name: new RegExp(`^bucket_data_${syncRules.
|
|
704
|
+
.listCollections({ name: new RegExp(`^bucket_data_${syncRules.replicationStreamId}_`) }, { nameOnly: true })
|
|
575
705
|
.toArray();
|
|
576
706
|
expect(
|
|
577
|
-
bucketCollections.some(
|
|
707
|
+
bucketCollections.some(
|
|
708
|
+
(collection) => collection.name === `bucket_data_${syncRules.replicationStreamId}_${firstBucket?.def}`
|
|
709
|
+
)
|
|
578
710
|
).toBe(true);
|
|
579
711
|
|
|
580
|
-
const syncRule = (await mongoFactory.db.sync_rules.findOne({
|
|
712
|
+
const syncRule = (await mongoFactory.db.sync_rules.findOne({
|
|
713
|
+
_id: syncRules.replicationStreamId
|
|
714
|
+
})) as ReplicationStreamDocumentV3;
|
|
581
715
|
const syncConfig = await db.syncConfigDefinitions.findOne({ _id: syncRule.sync_configs[0]._id });
|
|
582
716
|
const ruleMapping: SyncConfigDefinition['rule_mapping'] | undefined = syncConfig?.rule_mapping;
|
|
583
717
|
expect(Object.keys(ruleMapping?.definitions ?? {})).not.toHaveLength(0);
|
|
584
718
|
|
|
585
719
|
const parameterIndexId = Object.values(ruleMapping?.parameter_indexes ?? {})[0] as string | undefined;
|
|
586
720
|
expect(parameterIndexId).toBeDefined();
|
|
587
|
-
const parameterEntry = await db.
|
|
721
|
+
const parameterEntry = await db.parameterIndex(syncRules.replicationStreamId, parameterIndexId!).findOne({});
|
|
588
722
|
expect(deserializeParameterLookup(parameterEntry!.lookup)).toEqual(['shape-check']);
|
|
589
723
|
});
|
|
590
724
|
|
|
725
|
+
test.runIf(storageVersion >= 3)('replaces an existing deploying sync config', async () => {
|
|
726
|
+
await using factory = await storageConfig.factory();
|
|
727
|
+
|
|
728
|
+
const first = await factory.updateSyncRules(
|
|
729
|
+
updateSyncRulesFromYaml(
|
|
730
|
+
`
|
|
731
|
+
config:
|
|
732
|
+
edition: 2
|
|
733
|
+
|
|
734
|
+
streams:
|
|
735
|
+
by_owner:
|
|
736
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
737
|
+
`,
|
|
738
|
+
{ storageVersion }
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
const second = await factory.updateSyncRules(
|
|
742
|
+
updateSyncRulesFromYaml(
|
|
743
|
+
`
|
|
744
|
+
config:
|
|
745
|
+
edition: 2
|
|
746
|
+
|
|
747
|
+
streams:
|
|
748
|
+
by_project:
|
|
749
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
750
|
+
`,
|
|
751
|
+
{ storageVersion }
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
expect(second.replicationStreamId).not.toEqual(first.replicationStreamId);
|
|
756
|
+
expect(
|
|
757
|
+
(await factory.getStoppedReplicationStreams()).find(
|
|
758
|
+
(stream) => stream.replicationStreamId == first.replicationStreamId
|
|
759
|
+
)?.state
|
|
760
|
+
).toBe(storage.SyncRuleState.STOP);
|
|
761
|
+
|
|
762
|
+
const replicatingStreams = await factory.getReplicatingReplicationStreams();
|
|
763
|
+
expect(replicatingStreams).toHaveLength(1);
|
|
764
|
+
expect(replicatingStreams[0].replicationStreamId).toEqual(second.replicationStreamId);
|
|
765
|
+
|
|
766
|
+
const configs = second.syncConfigContent;
|
|
767
|
+
expect(configs).toHaveLength(1);
|
|
768
|
+
const statuses = await Promise.all(configs.map((config) => config.getSyncConfigStatus()));
|
|
769
|
+
expect(statuses.map((status) => status?.state)).toEqual([storage.SyncRuleState.PROCESSING]);
|
|
770
|
+
expect(statuses.map((status) => status?.id)).toEqual(configs.map((config) => config.syncConfigId));
|
|
771
|
+
const parsed = replicatingStreams[0].parsed(test_utils.PARSE_OPTIONS);
|
|
772
|
+
expect(parsed.syncConfigs).toHaveLength(1);
|
|
773
|
+
expect(parsed.hydratedSyncConfig.bucketDataSources).toHaveLength(1);
|
|
774
|
+
|
|
775
|
+
const bucketStorage = factory.getInstance(replicatingStreams[0]);
|
|
776
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
777
|
+
const resolved = await writer.resolveTables({
|
|
778
|
+
connection_id: 1,
|
|
779
|
+
source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
|
|
780
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834b')
|
|
781
|
+
});
|
|
782
|
+
expect(resolved.tables).toHaveLength(1);
|
|
783
|
+
expect(resolved.tables[0].bucketDataSources).toHaveLength(1);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test.runIf(storageVersion >= 3)('removing one config keeps shared source-table membership', async () => {
|
|
787
|
+
await using factory = await storageConfig.factory();
|
|
788
|
+
|
|
789
|
+
const first = await factory.updateSyncRules(
|
|
790
|
+
updateSyncRulesFromYaml(
|
|
791
|
+
`
|
|
792
|
+
config:
|
|
793
|
+
edition: 3
|
|
794
|
+
|
|
795
|
+
streams:
|
|
796
|
+
by_owner:
|
|
797
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
798
|
+
`,
|
|
799
|
+
{ storageVersion }
|
|
800
|
+
)
|
|
801
|
+
);
|
|
802
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
803
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
804
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
805
|
+
await firstWriter.commit('1/1');
|
|
806
|
+
|
|
807
|
+
const second = await factory.updateSyncRules(
|
|
808
|
+
updateSyncRulesFromYaml(
|
|
809
|
+
`
|
|
810
|
+
config:
|
|
811
|
+
edition: 3
|
|
812
|
+
|
|
813
|
+
streams:
|
|
814
|
+
by_project:
|
|
815
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
816
|
+
`,
|
|
817
|
+
{ storageVersion }
|
|
818
|
+
)
|
|
819
|
+
);
|
|
820
|
+
expect(second.replicationStreamId).toEqual(first.replicationStreamId);
|
|
821
|
+
|
|
822
|
+
const bucketStorage = factory.getInstance(second);
|
|
823
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
824
|
+
const source = sourceDescriptor('todos', { objectId: 'todos-relation' });
|
|
825
|
+
|
|
826
|
+
const resolved = await writer.resolveTables({
|
|
827
|
+
connection_id: 1,
|
|
828
|
+
source,
|
|
829
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834c')
|
|
830
|
+
});
|
|
831
|
+
expect(resolved.tables).toHaveLength(1);
|
|
832
|
+
expect(resolved.tables[0].bucketDataSources).toHaveLength(2);
|
|
833
|
+
|
|
834
|
+
await writer.markAllSnapshotDone('2/1');
|
|
835
|
+
await writer.commit('2/1');
|
|
836
|
+
|
|
837
|
+
const activeStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
|
|
838
|
+
await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
839
|
+
const activeStatus = await activeWriter.getSourceTableStatus(resolved.tables[0]);
|
|
840
|
+
expect(activeStatus?.bucketDataSources).toHaveLength(1);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test.runIf(storageVersion >= 3)(
|
|
844
|
+
'editing a stream query on the same table does not drop the active source table',
|
|
845
|
+
async () => {
|
|
846
|
+
await using factory = await storageConfig.factory();
|
|
847
|
+
|
|
848
|
+
const first = await factory.updateSyncRules(
|
|
849
|
+
updateSyncRulesFromYaml(
|
|
850
|
+
`
|
|
851
|
+
config:
|
|
852
|
+
edition: 3
|
|
853
|
+
|
|
854
|
+
streams:
|
|
855
|
+
todos:
|
|
856
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
857
|
+
`,
|
|
858
|
+
{ storageVersion }
|
|
859
|
+
)
|
|
860
|
+
);
|
|
861
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
862
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
863
|
+
const source = sourceDescriptor('todos', { objectId: 'todos-relation' });
|
|
864
|
+
const firstResolved = await firstWriter.resolveTables({
|
|
865
|
+
connection_id: 1,
|
|
866
|
+
source,
|
|
867
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834d')
|
|
868
|
+
});
|
|
869
|
+
expect(firstResolved.dropTables).toHaveLength(0);
|
|
870
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
871
|
+
await firstWriter.commit('1/1');
|
|
872
|
+
|
|
873
|
+
const second = await factory.updateSyncRules(
|
|
874
|
+
updateSyncRulesFromYaml(
|
|
875
|
+
`
|
|
876
|
+
config:
|
|
877
|
+
edition: 3
|
|
878
|
+
|
|
879
|
+
streams:
|
|
880
|
+
todos:
|
|
881
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
882
|
+
`,
|
|
883
|
+
{ storageVersion }
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
expect(second.replicationStreamId).toBe(first.replicationStreamId);
|
|
887
|
+
|
|
888
|
+
const deploying = await factory.getDeployingSyncConfig();
|
|
889
|
+
expect(deploying?.replicationStream.replicationStreamId).toBe(first.replicationStreamId);
|
|
890
|
+
const bucketStorage = deploying!.storage;
|
|
891
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
892
|
+
const resolved = await writer.resolveTables({
|
|
893
|
+
connection_id: 1,
|
|
894
|
+
source,
|
|
895
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834e')
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
expect(resolved.dropTables).toHaveLength(0);
|
|
899
|
+
expect(resolved.tables.map((table) => table.id.toString())).toEqual(['6544e3899293153fa7b3834e']);
|
|
900
|
+
expect(resolved.tables[0].bucketDataSources).toHaveLength(1);
|
|
901
|
+
|
|
902
|
+
const activeStatus = await firstWriter.getSourceTableStatus(firstResolved.tables[0]);
|
|
903
|
+
expect(activeStatus?.bucketDataSources).toHaveLength(1);
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
test.runIf(storageVersion >= 3)('reserves historical mapping ids without reusing stopped configs', async () => {
|
|
908
|
+
await using factory = await storageConfig.factory();
|
|
909
|
+
|
|
910
|
+
const ownerRules = `
|
|
911
|
+
config:
|
|
912
|
+
edition: 3
|
|
913
|
+
|
|
914
|
+
streams:
|
|
915
|
+
by_owner:
|
|
916
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
917
|
+
`;
|
|
918
|
+
const projectRules = `
|
|
919
|
+
config:
|
|
920
|
+
edition: 3
|
|
921
|
+
|
|
922
|
+
streams:
|
|
923
|
+
by_project:
|
|
924
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
925
|
+
`;
|
|
926
|
+
const statusRules = `
|
|
927
|
+
config:
|
|
928
|
+
edition: 3
|
|
929
|
+
|
|
930
|
+
streams:
|
|
931
|
+
by_status:
|
|
932
|
+
query: SELECT * FROM todos WHERE status = subscription.parameter('status')
|
|
933
|
+
`;
|
|
934
|
+
|
|
935
|
+
const first = await factory.updateSyncRules(updateSyncRulesFromYaml(ownerRules, { storageVersion }));
|
|
936
|
+
const firstStorage = factory.getInstance(first);
|
|
937
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
938
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
939
|
+
await firstWriter.commit('1/1');
|
|
940
|
+
|
|
941
|
+
const second = await factory.updateSyncRules(updateSyncRulesFromYaml(projectRules, { storageVersion }));
|
|
942
|
+
expect(second.replicationStreamId).toBe(first.replicationStreamId);
|
|
943
|
+
|
|
944
|
+
const third = await factory.updateSyncRules(updateSyncRulesFromYaml(statusRules, { storageVersion }));
|
|
945
|
+
expect(third.replicationStreamId).toBe(first.replicationStreamId);
|
|
946
|
+
|
|
947
|
+
const configs = await getMongoSyncConfigContents(factory, first.replicationStreamId);
|
|
948
|
+
const statuses = await Promise.all(
|
|
949
|
+
configs.map(async (config) => [config, await config.getSyncConfigStatus()] as const)
|
|
950
|
+
);
|
|
951
|
+
// ownerRules
|
|
952
|
+
const activeConfig = statuses.find(([, status]) => status?.state == storage.SyncRuleState.ACTIVE)?.[0];
|
|
953
|
+
// projectRules
|
|
954
|
+
const stoppedConfig = statuses.find(([, status]) => status?.state == storage.SyncRuleState.STOP)?.[0];
|
|
955
|
+
// statusRules
|
|
956
|
+
const processingConfig = statuses.find(([, status]) => status?.state == storage.SyncRuleState.PROCESSING)?.[0];
|
|
957
|
+
expect(stoppedConfig).toBeDefined();
|
|
958
|
+
expect(activeConfig).toBeDefined();
|
|
959
|
+
expect(processingConfig).toBeDefined();
|
|
960
|
+
|
|
961
|
+
const activePrefix = bucketRequest(activeConfig!, 'by_owner|0["owner"]').bucket;
|
|
962
|
+
const stoppedPrefix = bucketRequest(stoppedConfig!, 'by_project|0["project"]').bucket;
|
|
963
|
+
const processingPrefix = bucketRequest(processingConfig!, 'by_status|0["status"]').bucket;
|
|
964
|
+
|
|
965
|
+
expect(processingPrefix).not.toBe(stoppedPrefix);
|
|
966
|
+
expect(processingPrefix).not.toBe(activePrefix);
|
|
967
|
+
expect(bucketDefinitionId(processingPrefix)).toBeGreaterThan(
|
|
968
|
+
Math.max(bucketDefinitionId(stoppedPrefix), bucketDefinitionId(activePrefix))
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
test.runIf(storageVersion >= 3)('table snapshot status only affects sync configs using that table', async () => {
|
|
973
|
+
await using factory = await storageConfig.factory();
|
|
974
|
+
|
|
975
|
+
const first = await factory.updateSyncRules(
|
|
976
|
+
updateSyncRulesFromYaml(
|
|
977
|
+
`
|
|
978
|
+
config:
|
|
979
|
+
edition: 3
|
|
980
|
+
|
|
981
|
+
streams:
|
|
982
|
+
by_owner:
|
|
983
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
984
|
+
`,
|
|
985
|
+
{ storageVersion }
|
|
986
|
+
)
|
|
987
|
+
);
|
|
988
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
989
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
990
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
991
|
+
await firstWriter.commit('1/1');
|
|
992
|
+
|
|
993
|
+
const second = await factory.updateSyncRules(
|
|
994
|
+
updateSyncRulesFromYaml(
|
|
995
|
+
`
|
|
996
|
+
config:
|
|
997
|
+
edition: 3
|
|
998
|
+
|
|
999
|
+
streams:
|
|
1000
|
+
by_owner:
|
|
1001
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1002
|
+
by_project:
|
|
1003
|
+
query: SELECT * FROM projects WHERE id = subscription.parameter('project_id')
|
|
1004
|
+
`,
|
|
1005
|
+
{ storageVersion }
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
expect(second.replicationStreamId).toBe(first.replicationStreamId);
|
|
1009
|
+
|
|
1010
|
+
const bucketStorage = factory.getInstance(second);
|
|
1011
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1012
|
+
const resolved = await writer.resolveTables({
|
|
1013
|
+
connection_id: 1,
|
|
1014
|
+
source: sourceDescriptor('projects', { objectId: 'projects-relation' }),
|
|
1015
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b3834d')
|
|
1016
|
+
});
|
|
1017
|
+
expect(resolved.tables).toHaveLength(1);
|
|
1018
|
+
|
|
1019
|
+
await writer.markAllSnapshotDone('2/1');
|
|
1020
|
+
await writer.markTableSnapshotRequired(resolved.tables[0]);
|
|
1021
|
+
|
|
1022
|
+
const configsAfterRequired = await getMongoSyncConfigContents(factory, first.replicationStreamId);
|
|
1023
|
+
const statusesAfterRequired = await Promise.all(configsAfterRequired.map((config) => config.getSyncConfigStatus()));
|
|
1024
|
+
const ownerConfigAfterRequired = statusesAfterRequired.find(
|
|
1025
|
+
(status) => status?.state == storage.SyncRuleState.ACTIVE
|
|
1026
|
+
);
|
|
1027
|
+
const projectConfigAfterRequired = statusesAfterRequired.find(
|
|
1028
|
+
(status) => status?.state == storage.SyncRuleState.PROCESSING
|
|
1029
|
+
);
|
|
1030
|
+
expect(ownerConfigAfterRequired?.snapshot_done).toBe(true);
|
|
1031
|
+
expect(projectConfigAfterRequired?.snapshot_done).toBe(false);
|
|
1032
|
+
|
|
1033
|
+
await writer.markTableSnapshotDone(resolved.tables, '3/1');
|
|
1034
|
+
|
|
1035
|
+
const configsAfterDone = await getMongoSyncConfigContents(factory, first.replicationStreamId);
|
|
1036
|
+
const statusesAfterDone = await Promise.all(configsAfterDone.map((config) => config.getSyncConfigStatus()));
|
|
1037
|
+
const ownerConfigAfterDone = statusesAfterDone.find((status) => status?.id == ownerConfigAfterRequired!.id);
|
|
1038
|
+
const projectConfigAfterDone = statusesAfterDone.find((status) => status?.id == projectConfigAfterRequired!.id);
|
|
1039
|
+
expect(ownerConfigAfterDone?.snapshot_done).toBe(true);
|
|
1040
|
+
expect(projectConfigAfterDone?.snapshot_done).toBe(false);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test.runIf(storageVersion >= 3)(
|
|
1044
|
+
'keeps compatible active and deploying sync configs in one replication stream',
|
|
1045
|
+
async () => {
|
|
1046
|
+
await using factory = await storageConfig.factory();
|
|
1047
|
+
|
|
1048
|
+
const first = await factory.updateSyncRules(
|
|
1049
|
+
updateSyncRulesFromYaml(
|
|
1050
|
+
`
|
|
1051
|
+
config:
|
|
1052
|
+
edition: 3
|
|
1053
|
+
|
|
1054
|
+
streams:
|
|
1055
|
+
by_owner:
|
|
1056
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1057
|
+
`,
|
|
1058
|
+
{ storageVersion }
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
1062
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1063
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1064
|
+
await firstWriter.commit('1/1');
|
|
1065
|
+
const initialActiveStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
|
|
1066
|
+
|
|
1067
|
+
let configs = first.syncConfigContent;
|
|
1068
|
+
const firstConfigId = configs[0].syncConfigId;
|
|
1069
|
+
expect((await factory.getActiveSyncConfig())?.content.syncConfigId).toBe(firstConfigId);
|
|
1070
|
+
|
|
1071
|
+
const second = await factory.updateSyncRules(
|
|
1072
|
+
updateSyncRulesFromYaml(
|
|
1073
|
+
`
|
|
1074
|
+
config:
|
|
1075
|
+
edition: 3
|
|
1076
|
+
|
|
1077
|
+
streams:
|
|
1078
|
+
by_project:
|
|
1079
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
1080
|
+
`,
|
|
1081
|
+
{ storageVersion }
|
|
1082
|
+
)
|
|
1083
|
+
);
|
|
1084
|
+
expect(second.replicationStreamId).toEqual(first.replicationStreamId);
|
|
1085
|
+
|
|
1086
|
+
configs = await getMongoSyncConfigContents(factory, first.replicationStreamId);
|
|
1087
|
+
const deploying = await factory.getDeployingSyncConfig();
|
|
1088
|
+
expect(configs).toHaveLength(2);
|
|
1089
|
+
expect(deploying).not.toBeNull();
|
|
1090
|
+
expect((await factory.getActiveSyncConfig())?.content.syncConfigId).toBe(firstConfigId);
|
|
1091
|
+
|
|
1092
|
+
const stream = (await factory.getActiveSyncConfig())?.replicationStream;
|
|
1093
|
+
expect(stream?.state).toBe(storage.SyncRuleState.ACTIVE);
|
|
1094
|
+
expect(
|
|
1095
|
+
(await Promise.all(configs.map((config) => config.getSyncConfigStatus()))).map((status) => status?.state).sort()
|
|
1096
|
+
).toEqual([storage.SyncRuleState.ACTIVE, storage.SyncRuleState.PROCESSING]);
|
|
1097
|
+
|
|
1098
|
+
const replicatingStreams = await factory.getReplicatingReplicationStreams();
|
|
1099
|
+
expect(replicatingStreams).toHaveLength(1);
|
|
1100
|
+
for (const config of configs) {
|
|
1101
|
+
expect(replicatingStreams[0].replicationJobId).toContain(config.syncConfigId);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorage;
|
|
1105
|
+
await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1106
|
+
await secondWriter.markAllSnapshotDone('2/1');
|
|
1107
|
+
await secondWriter.commit('2/1');
|
|
1108
|
+
|
|
1109
|
+
const updatedStream = (await factory.getActiveSyncConfig())?.replicationStream;
|
|
1110
|
+
expect(updatedStream?.state).toBe(storage.SyncRuleState.ACTIVE);
|
|
1111
|
+
const updatedConfigs = await getMongoSyncConfigContents(factory, first.replicationStreamId);
|
|
1112
|
+
expect(
|
|
1113
|
+
(await Promise.all(updatedConfigs.map((config) => config.getSyncConfigStatus())))
|
|
1114
|
+
.map((status) => status?.state)
|
|
1115
|
+
.sort()
|
|
1116
|
+
).toEqual([storage.SyncRuleState.ACTIVE, storage.SyncRuleState.STOP]);
|
|
1117
|
+
expect(await factory.getDeployingSyncConfig()).toBeNull();
|
|
1118
|
+
expect((await factory.getActiveSyncConfig())?.content.syncConfigId).not.toBe(firstConfigId);
|
|
1119
|
+
const updatedActiveStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
|
|
1120
|
+
expect(updatedActiveStorage.replicationStream.replicationJobId).not.toBe(
|
|
1121
|
+
initialActiveStorage.replicationStream.replicationJobId
|
|
1122
|
+
);
|
|
1123
|
+
expect(updatedActiveStorage.replicationStream.replicationJobId).toContain(
|
|
1124
|
+
(await factory.getActiveSyncConfig())!.content.syncConfigId
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
test.runIf(storageVersion >= 3)(
|
|
1130
|
+
'appended compatible config adopts the stream checkpoint instead of regressing to 0',
|
|
1131
|
+
async () => {
|
|
1132
|
+
await using factory = await storageConfig.factory();
|
|
1133
|
+
|
|
1134
|
+
// First config replicates some data, advancing the stream-level op head and its checkpoint.
|
|
1135
|
+
const first = await factory.updateSyncRules(
|
|
1136
|
+
updateSyncRulesFromYaml(
|
|
1137
|
+
`
|
|
1138
|
+
config:
|
|
1139
|
+
edition: 3
|
|
1140
|
+
|
|
1141
|
+
streams:
|
|
1142
|
+
by_owner:
|
|
1143
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1144
|
+
`,
|
|
1145
|
+
{ storageVersion }
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
1148
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
1149
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1150
|
+
const sourceTable = await test_utils.resolveTestTable(
|
|
1151
|
+
firstWriter,
|
|
1152
|
+
'todos',
|
|
1153
|
+
['id'],
|
|
1154
|
+
INITIALIZED_MONGO_STORAGE_FACTORY
|
|
1155
|
+
);
|
|
1156
|
+
await firstWriter.save({
|
|
1157
|
+
sourceTable,
|
|
1158
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
1159
|
+
after: {
|
|
1160
|
+
id: 'todo-1',
|
|
1161
|
+
owner_id: 'user-1',
|
|
1162
|
+
project_id: 'project-1'
|
|
1163
|
+
},
|
|
1164
|
+
afterReplicaId: test_utils.rid('todo-1')
|
|
1165
|
+
});
|
|
1166
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1167
|
+
await firstWriter.commit('1/1');
|
|
1168
|
+
|
|
1169
|
+
const firstCheckpoint = (await firstStorage.getCheckpoint()).checkpoint;
|
|
1170
|
+
expect(firstCheckpoint).toBeGreaterThan(0n);
|
|
1171
|
+
|
|
1172
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
1173
|
+
const streamDocBefore = (await mongoFactory.db.sync_rules.findOne({
|
|
1174
|
+
_id: first.replicationStreamId
|
|
1175
|
+
})) as ReplicationStreamDocumentV3;
|
|
1176
|
+
// The stream-level head was advanced durably to (at least) the first config's checkpoint.
|
|
1177
|
+
expect(streamDocBefore.last_persisted_op).not.toBeNull();
|
|
1178
|
+
expect(BigInt(streamDocBefore.last_persisted_op!)).toBeGreaterThanOrEqual(firstCheckpoint);
|
|
1179
|
+
|
|
1180
|
+
// Append a compatible second config that replicates nothing new.
|
|
1181
|
+
const second = await factory.updateSyncRules(
|
|
1182
|
+
updateSyncRulesFromYaml(
|
|
1183
|
+
`
|
|
1184
|
+
config:
|
|
1185
|
+
edition: 3
|
|
1186
|
+
|
|
1187
|
+
streams:
|
|
1188
|
+
by_project:
|
|
1189
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
1190
|
+
`,
|
|
1191
|
+
{ storageVersion }
|
|
1192
|
+
)
|
|
1193
|
+
);
|
|
1194
|
+
expect(second.replicationStreamId).toEqual(first.replicationStreamId);
|
|
1195
|
+
|
|
1196
|
+
const replicatingStreams = await factory.getReplicatingReplicationStreams();
|
|
1197
|
+
expect(replicatingStreams).toHaveLength(1);
|
|
1198
|
+
const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorage;
|
|
1199
|
+
await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1200
|
+
// No new data replicated - just complete the snapshot and commit.
|
|
1201
|
+
await secondWriter.markAllSnapshotDone('2/1');
|
|
1202
|
+
await secondWriter.commit('2/1');
|
|
1203
|
+
|
|
1204
|
+
// The appended config's checkpoint must adopt the stream head, not regress to 0.
|
|
1205
|
+
const streamDocAfter = (await mongoFactory.db.sync_rules.findOne({
|
|
1206
|
+
_id: first.replicationStreamId
|
|
1207
|
+
})) as ReplicationStreamDocumentV3;
|
|
1208
|
+
const head = BigInt(streamDocAfter.last_persisted_op!);
|
|
1209
|
+
for (const config of streamDocAfter.sync_configs) {
|
|
1210
|
+
expect(config.last_checkpoint).not.toBeNull();
|
|
1211
|
+
expect(BigInt(config.last_checkpoint!)).toEqual(head);
|
|
1212
|
+
}
|
|
1213
|
+
// All configs share the same checkpoint, equal to the stream head.
|
|
1214
|
+
expect(head).toBeGreaterThanOrEqual(firstCheckpoint);
|
|
1215
|
+
|
|
1216
|
+
// After activation, the active config's checkpoint does not regress.
|
|
1217
|
+
const activeStorage = (await factory.getActiveSyncConfig())?.storage as MongoSyncBucketStorage;
|
|
1218
|
+
const activeCheckpoint = (await activeStorage.getCheckpoint()).checkpoint;
|
|
1219
|
+
expect(activeCheckpoint).toBeGreaterThanOrEqual(firstCheckpoint);
|
|
1220
|
+
}
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
test.runIf(storageVersion >= 3)('creates a new replication stream when compatibility options differ', async () => {
|
|
1224
|
+
await using factory = await storageConfig.factory();
|
|
1225
|
+
|
|
1226
|
+
const first = await factory.updateSyncRules(
|
|
1227
|
+
updateSyncRulesFromYaml(
|
|
1228
|
+
`
|
|
1229
|
+
config:
|
|
1230
|
+
edition: 3
|
|
1231
|
+
|
|
1232
|
+
streams:
|
|
1233
|
+
by_owner:
|
|
1234
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1235
|
+
`,
|
|
1236
|
+
{ storageVersion }
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1239
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
1240
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1241
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1242
|
+
await firstWriter.commit('1/1');
|
|
1243
|
+
|
|
1244
|
+
// Same streams, but a different compatibility edition - must not append to the active stream.
|
|
1245
|
+
const second = await factory.updateSyncRules(
|
|
1246
|
+
updateSyncRulesFromYaml(
|
|
1247
|
+
`
|
|
1248
|
+
config:
|
|
1249
|
+
edition: 2
|
|
1250
|
+
|
|
1251
|
+
streams:
|
|
1252
|
+
by_owner:
|
|
1253
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1254
|
+
`,
|
|
1255
|
+
{ storageVersion }
|
|
1256
|
+
)
|
|
1257
|
+
);
|
|
1258
|
+
expect(second.replicationStreamId).not.toEqual(first.replicationStreamId);
|
|
1259
|
+
|
|
1260
|
+
// The first stream stays active until the replacement has replicated.
|
|
1261
|
+
const active = await factory.getActiveSyncConfig();
|
|
1262
|
+
expect(active?.replicationStream.replicationStreamId).toEqual(first.replicationStreamId);
|
|
1263
|
+
expect((await factory.getDeployingSyncConfig())?.replicationStream.replicationStreamId).toEqual(
|
|
1264
|
+
second.replicationStreamId
|
|
1265
|
+
);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
test.runIf(storageVersion >= 3)(
|
|
1269
|
+
'creates a new replication stream for legacy sync rules without a serialized plan',
|
|
1270
|
+
async () => {
|
|
1271
|
+
await using factory = await storageConfig.factory();
|
|
1272
|
+
|
|
1273
|
+
const first = await factory.updateSyncRules(
|
|
1274
|
+
updateSyncRulesFromYaml(
|
|
1275
|
+
`
|
|
1276
|
+
config:
|
|
1277
|
+
edition: 3
|
|
1278
|
+
|
|
1279
|
+
streams:
|
|
1280
|
+
by_owner:
|
|
1281
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1282
|
+
`,
|
|
1283
|
+
{ storageVersion }
|
|
1284
|
+
)
|
|
1285
|
+
);
|
|
1286
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
1287
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1288
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1289
|
+
await firstWriter.commit('1/1');
|
|
1290
|
+
|
|
1291
|
+
// Legacy sync rules have no serialized plan - must not append to the active stream.
|
|
1292
|
+
const second = await factory.updateSyncRules(updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion }));
|
|
1293
|
+
expect(second.replicationStreamId).not.toEqual(first.replicationStreamId);
|
|
1294
|
+
}
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
test.runIf(storageVersion >= 3)('does not append to an active legacy sync config', async () => {
|
|
1298
|
+
await using factory = await storageConfig.factory();
|
|
1299
|
+
|
|
1300
|
+
const first = await factory.updateSyncRules(updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion }));
|
|
1301
|
+
const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
|
|
1302
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1303
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1304
|
+
await firstWriter.commit('1/1');
|
|
1305
|
+
|
|
1306
|
+
// The active config has no serialized plan, so a new streams config must not be appended to it.
|
|
1307
|
+
const second = await factory.updateSyncRules(
|
|
1308
|
+
updateSyncRulesFromYaml(
|
|
1309
|
+
`
|
|
1310
|
+
config:
|
|
1311
|
+
edition: 3
|
|
1312
|
+
|
|
1313
|
+
streams:
|
|
1314
|
+
by_owner:
|
|
1315
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1316
|
+
`,
|
|
1317
|
+
{ storageVersion }
|
|
1318
|
+
)
|
|
1319
|
+
);
|
|
1320
|
+
expect(second.replicationStreamId).not.toEqual(first.replicationStreamId);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
591
1323
|
test.runIf(storageVersion < 3)('can replace processing legacy sync rules', async () => {
|
|
592
1324
|
await using factory = await storageConfig.factory();
|
|
593
1325
|
|
|
@@ -600,7 +1332,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
600
1332
|
).resolves.toBeDefined();
|
|
601
1333
|
|
|
602
1334
|
const mongoFactory = factory as MongoBucketStorage;
|
|
603
|
-
expect((await mongoFactory.db.sync_rules.findOne({ _id: firstSyncRules.
|
|
1335
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: firstSyncRules.replicationStreamId }))?.state).toBe(
|
|
604
1336
|
storage.SyncRuleState.STOP
|
|
605
1337
|
);
|
|
606
1338
|
});
|
|
@@ -612,10 +1344,48 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
612
1344
|
updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion, lock: true })
|
|
613
1345
|
);
|
|
614
1346
|
|
|
615
|
-
expect(syncRules.current_lock?.sync_rules_id).toBe(syncRules.
|
|
1347
|
+
expect(syncRules.current_lock?.sync_rules_id).toBe(syncRules.replicationStreamId);
|
|
616
1348
|
await syncRules.current_lock?.release();
|
|
617
1349
|
});
|
|
618
1350
|
|
|
1351
|
+
test.runIf(storageVersion >= 3)('does not lock when appending a sync config to an existing stream', async () => {
|
|
1352
|
+
await using factory = await storageConfig.factory();
|
|
1353
|
+
|
|
1354
|
+
const firstRules = `
|
|
1355
|
+
config:
|
|
1356
|
+
edition: 3
|
|
1357
|
+
|
|
1358
|
+
streams:
|
|
1359
|
+
by_owner:
|
|
1360
|
+
query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
|
|
1361
|
+
`;
|
|
1362
|
+
const secondRules = `
|
|
1363
|
+
config:
|
|
1364
|
+
edition: 3
|
|
1365
|
+
|
|
1366
|
+
streams:
|
|
1367
|
+
by_project:
|
|
1368
|
+
query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
|
|
1369
|
+
`;
|
|
1370
|
+
|
|
1371
|
+
const first = await factory.updateSyncRules(updateSyncRulesFromYaml(firstRules, { storageVersion, lock: true }));
|
|
1372
|
+
expect(first.current_lock?.sync_rules_id).toBe(first.replicationStreamId);
|
|
1373
|
+
try {
|
|
1374
|
+
const firstStorage = factory.getInstance(first);
|
|
1375
|
+
await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
1376
|
+
await firstWriter.markAllSnapshotDone('1/1');
|
|
1377
|
+
await firstWriter.commit('1/1');
|
|
1378
|
+
|
|
1379
|
+
const second = await factory.updateSyncRules(
|
|
1380
|
+
updateSyncRulesFromYaml(secondRules, { storageVersion, lock: true })
|
|
1381
|
+
);
|
|
1382
|
+
expect(second.replicationStreamId).toBe(first.replicationStreamId);
|
|
1383
|
+
expect(second.current_lock).toBeNull();
|
|
1384
|
+
} finally {
|
|
1385
|
+
await first.current_lock?.release();
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
|
|
619
1389
|
test.runIf(storageVersion < 3)('uses a single current_data collection for v1 source records', async () => {
|
|
620
1390
|
await using factory = await storageConfig.factory();
|
|
621
1391
|
const syncRules = await factory.updateSyncRules(
|
|
@@ -647,10 +1417,10 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
647
1417
|
await writer.commit('1/1');
|
|
648
1418
|
|
|
649
1419
|
const mongoFactory = factory as MongoBucketStorage;
|
|
650
|
-
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.
|
|
1420
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.replicationStreamId })).toBe(1);
|
|
651
1421
|
|
|
652
1422
|
const sourceRecordCollections = await mongoFactory.db.db
|
|
653
|
-
.listCollections({ name: new RegExp(`^source_records_${syncRules.
|
|
1423
|
+
.listCollections({ name: new RegExp(`^source_records_${syncRules.replicationStreamId}_`) }, { nameOnly: true })
|
|
654
1424
|
.toArray();
|
|
655
1425
|
expect(sourceRecordCollections).toEqual([]);
|
|
656
1426
|
});
|
|
@@ -686,11 +1456,11 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
686
1456
|
await writer.commit('1/1');
|
|
687
1457
|
|
|
688
1458
|
const mongoFactory = factory as MongoBucketStorage;
|
|
689
|
-
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.
|
|
1459
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.replicationStreamId })).toBe(1);
|
|
690
1460
|
|
|
691
1461
|
await bucketStorage.clear();
|
|
692
1462
|
|
|
693
|
-
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.
|
|
1463
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.replicationStreamId })).toBe(0);
|
|
694
1464
|
});
|
|
695
1465
|
|
|
696
1466
|
test.runIf(storageVersion < 3)('storage metrics include v1 current_data', async () => {
|
|
@@ -725,7 +1495,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
725
1495
|
await writer.commit('1/1');
|
|
726
1496
|
|
|
727
1497
|
const mongoFactory = factory as MongoBucketStorage;
|
|
728
|
-
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.
|
|
1498
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.replicationStreamId })).toBe(1);
|
|
729
1499
|
|
|
730
1500
|
const metricsAfter = await factory.getStorageMetrics();
|
|
731
1501
|
expect(metricsAfter.replication_size_bytes).toBeGreaterThan(metricsBefore.replication_size_bytes);
|
|
@@ -783,7 +1553,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
783
1553
|
}
|
|
784
1554
|
);
|
|
785
1555
|
|
|
786
|
-
test.runIf(storageVersion
|
|
1556
|
+
test.runIf(storageVersion == 3)('cleans pending deletes only for tracked v3 source tables', async () => {
|
|
787
1557
|
await using factory = await storageConfig.factory();
|
|
788
1558
|
const syncRules = await factory.updateSyncRules(
|
|
789
1559
|
updateSyncRulesFromYaml(
|
|
@@ -798,21 +1568,20 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
798
1568
|
);
|
|
799
1569
|
|
|
800
1570
|
const mongoFactory = factory as MongoBucketStorage;
|
|
801
|
-
const bucketStorage = mongoFactory.getInstance(syncRules)
|
|
802
|
-
const db = bucketStorage.db;
|
|
803
|
-
await db.initializeStreamStorage(syncRules.
|
|
1571
|
+
const bucketStorage = mongoFactory.getInstance(syncRules);
|
|
1572
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1573
|
+
await db.initializeStreamStorage(syncRules.replicationStreamId);
|
|
804
1574
|
|
|
805
1575
|
const sourceTableA = new bson.ObjectId();
|
|
806
1576
|
const sourceTableB = new bson.ObjectId();
|
|
807
|
-
await db.
|
|
1577
|
+
await db.sourceTables(syncRules.replicationStreamId).insertMany([
|
|
808
1578
|
{
|
|
809
1579
|
_id: sourceTableA,
|
|
810
1580
|
connection_id: 1,
|
|
811
1581
|
relation_id: 'a',
|
|
812
1582
|
schema_name: 'public',
|
|
813
1583
|
table_name: 'table_a',
|
|
814
|
-
replica_id_columns:
|
|
815
|
-
replica_id_columns2: [],
|
|
1584
|
+
replica_id_columns: [],
|
|
816
1585
|
snapshot_done: true,
|
|
817
1586
|
snapshot_status: undefined,
|
|
818
1587
|
bucket_data_source_ids: [],
|
|
@@ -825,8 +1594,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
825
1594
|
relation_id: 'b',
|
|
826
1595
|
schema_name: 'public',
|
|
827
1596
|
table_name: 'table_b',
|
|
828
|
-
replica_id_columns:
|
|
829
|
-
replica_id_columns2: [],
|
|
1597
|
+
replica_id_columns: [],
|
|
830
1598
|
snapshot_done: true,
|
|
831
1599
|
snapshot_status: undefined,
|
|
832
1600
|
bucket_data_source_ids: [],
|
|
@@ -835,35 +1603,53 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
835
1603
|
}
|
|
836
1604
|
]);
|
|
837
1605
|
|
|
838
|
-
await db.
|
|
1606
|
+
await db.sourceRecords(syncRules.replicationStreamId, sourceTableA).insertMany([
|
|
839
1607
|
{ _id: 'deleted-1', data: null, buckets: [], lookups: [], pending_delete: 5n },
|
|
840
1608
|
{ _id: 'deleted-2', data: null, buckets: [], lookups: [], pending_delete: 9n },
|
|
841
1609
|
{ _id: 'active', data: null, buckets: [], lookups: [] }
|
|
842
1610
|
]);
|
|
843
1611
|
await db
|
|
844
|
-
.
|
|
1612
|
+
.sourceRecords(syncRules.replicationStreamId, sourceTableB)
|
|
845
1613
|
.insertMany([{ _id: 'later-delete', data: null, buckets: [], lookups: [], pending_delete: 12n }]);
|
|
846
1614
|
|
|
847
|
-
const store = new SourceRecordStoreV3(
|
|
1615
|
+
const store = new SourceRecordStoreV3(
|
|
1616
|
+
db,
|
|
1617
|
+
syncRules.replicationStreamId,
|
|
1618
|
+
bucketStorage.replicationStream.syncConfigContent[0].mapping
|
|
1619
|
+
);
|
|
848
1620
|
const logger = { info() {} } as any;
|
|
849
1621
|
|
|
850
1622
|
await store.postCommitCleanup(6n, logger);
|
|
851
1623
|
|
|
852
|
-
expect(
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
expect(
|
|
856
|
-
|
|
1624
|
+
expect(
|
|
1625
|
+
await db.sourceRecords(syncRules.replicationStreamId, sourceTableA).countDocuments({ pending_delete: 5n })
|
|
1626
|
+
).toBe(0);
|
|
1627
|
+
expect(
|
|
1628
|
+
await db.sourceRecords(syncRules.replicationStreamId, sourceTableA).countDocuments({ pending_delete: 9n })
|
|
1629
|
+
).toBe(1);
|
|
1630
|
+
expect(
|
|
1631
|
+
await db.sourceRecords(syncRules.replicationStreamId, sourceTableB).countDocuments({ pending_delete: 12n })
|
|
1632
|
+
).toBe(1);
|
|
1633
|
+
expect(
|
|
1634
|
+
(await db.sourceTables(syncRules.replicationStreamId).findOne({ _id: sourceTableA }))?.latest_pending_delete
|
|
1635
|
+
).toBe(9n);
|
|
1636
|
+
expect(
|
|
1637
|
+
(await db.sourceTables(syncRules.replicationStreamId).findOne({ _id: sourceTableB }))?.latest_pending_delete
|
|
1638
|
+
).toBe(12n);
|
|
857
1639
|
|
|
858
1640
|
await store.postCommitCleanup(10n, logger);
|
|
859
1641
|
|
|
860
1642
|
expect(
|
|
861
|
-
await db
|
|
1643
|
+
await db
|
|
1644
|
+
.sourceRecords(syncRules.replicationStreamId, sourceTableA)
|
|
1645
|
+
.countDocuments({ pending_delete: { $exists: true } })
|
|
862
1646
|
).toBe(0);
|
|
863
1647
|
expect(
|
|
864
|
-
(await db.
|
|
1648
|
+
(await db.sourceTables(syncRules.replicationStreamId).findOne({ _id: sourceTableA }))?.latest_pending_delete
|
|
865
1649
|
).toBeUndefined();
|
|
866
|
-
expect(
|
|
1650
|
+
expect(
|
|
1651
|
+
(await db.sourceTables(syncRules.replicationStreamId).findOne({ _id: sourceTableB }))?.latest_pending_delete
|
|
1652
|
+
).toBe(12n);
|
|
867
1653
|
});
|
|
868
1654
|
}
|
|
869
1655
|
|
|
@@ -880,7 +1666,7 @@ describe('sync - mongodb', () => {
|
|
|
880
1666
|
await legacyWriter.markAllSnapshotDone('1/1');
|
|
881
1667
|
await legacyWriter.commit('1/1');
|
|
882
1668
|
|
|
883
|
-
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.
|
|
1669
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.replicationStreamId }))?.state).toBe(
|
|
884
1670
|
storage.SyncRuleState.ACTIVE
|
|
885
1671
|
);
|
|
886
1672
|
|
|
@@ -892,7 +1678,7 @@ describe('sync - mongodb', () => {
|
|
|
892
1678
|
await v3Writer.markAllSnapshotDone('2/1');
|
|
893
1679
|
await v3Writer.commit('2/1');
|
|
894
1680
|
|
|
895
|
-
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.
|
|
1681
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.replicationStreamId }))?.state).toBe(
|
|
896
1682
|
storage.SyncRuleState.STOP
|
|
897
1683
|
);
|
|
898
1684
|
});
|
|
@@ -900,6 +1686,229 @@ describe('sync - mongodb', () => {
|
|
|
900
1686
|
for (const storageVersion of TEST_STORAGE_VERSIONS) {
|
|
901
1687
|
describe(`storage v${storageVersion}`, () => {
|
|
902
1688
|
registerSyncStorageTests(INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion);
|
|
1689
|
+
|
|
1690
|
+
describe.runIf(storageVersion == 3)('V3 read filtering boundaries', () => {
|
|
1691
|
+
async function setupFilteringTest() {
|
|
1692
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
1693
|
+
const syncRules = await factory.updateSyncRules(
|
|
1694
|
+
updateSyncRulesFromYaml(
|
|
1695
|
+
`
|
|
1696
|
+
bucket_definitions:
|
|
1697
|
+
global:
|
|
1698
|
+
data:
|
|
1699
|
+
- SELECT id, description FROM test
|
|
1700
|
+
`,
|
|
1701
|
+
{ storageVersion }
|
|
1702
|
+
)
|
|
1703
|
+
);
|
|
1704
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
1705
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1706
|
+
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1707
|
+
|
|
1708
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', 0n);
|
|
1709
|
+
const definitionId = mapping.bucketSourceId(request.source);
|
|
1710
|
+
const collection = db.bucketData(syncRules.replicationStreamId, definitionId);
|
|
1711
|
+
|
|
1712
|
+
const bucketName = request.bucket;
|
|
1713
|
+
const sourceTable = new bson.ObjectId();
|
|
1714
|
+
const bucketKey: BucketKey = {
|
|
1715
|
+
replicationStreamId: syncRules.replicationStreamId,
|
|
1716
|
+
definitionId,
|
|
1717
|
+
bucket: bucketName
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
function makeOps(opIds: bigint[]): BucketDataDoc[] {
|
|
1721
|
+
return opIds.map((opId) => ({
|
|
1722
|
+
bucketKey,
|
|
1723
|
+
o: opId,
|
|
1724
|
+
op: 'PUT' as const,
|
|
1725
|
+
source_table: sourceTable,
|
|
1726
|
+
source_key: test_utils.rid(`row-${opId}`),
|
|
1727
|
+
table: 'items',
|
|
1728
|
+
row_id: `row-${opId}`,
|
|
1729
|
+
checksum: BigInt(opId) * 10n,
|
|
1730
|
+
data: `{"id":"row-${opId}"}`
|
|
1731
|
+
}));
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const docA = serializeBucketData(bucketName, makeOps([10n, 20n, 30n]));
|
|
1735
|
+
const docB = serializeBucketData(bucketName, makeOps([40n, 50n, 60n]));
|
|
1736
|
+
const docC = serializeBucketData(bucketName, makeOps([70n, 80n, 90n]));
|
|
1737
|
+
|
|
1738
|
+
await collection.insertMany([docA, docB, docC]);
|
|
1739
|
+
|
|
1740
|
+
return { factory, syncRules, bucketStorage, bucketName };
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
async function getFilteredOps(start: number, checkpoint: number): Promise<bigint[]> {
|
|
1744
|
+
const { syncRules, bucketStorage } = await setupFilteringTest();
|
|
1745
|
+
const request = bucketRequest(syncRules.syncConfigContent[0], 'global[]', BigInt(start));
|
|
1746
|
+
const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(BigInt(checkpoint), [request]));
|
|
1747
|
+
const ops = batch.flatMap((b) => b.chunkData.data.map((d) => BigInt(d.op_id)));
|
|
1748
|
+
return ops;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
test('case 1: start=5, checkpoint=95 → all ops', async () => {
|
|
1752
|
+
const ops = await getFilteredOps(5, 95);
|
|
1753
|
+
expect(ops).toEqual([10n, 20n, 30n, 40n, 50n, 60n, 70n, 80n, 90n]);
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
test('case 2: start=10, checkpoint=90 → ops in (10,90]', async () => {
|
|
1757
|
+
const ops = await getFilteredOps(10, 90);
|
|
1758
|
+
expect(ops).toEqual([20n, 30n, 40n, 50n, 60n, 70n, 80n, 90n]);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
test('case 3: start=15, checkpoint=85 → partial doc boundaries', async () => {
|
|
1762
|
+
const ops = await getFilteredOps(15, 85);
|
|
1763
|
+
expect(ops).toEqual([20n, 30n, 40n, 50n, 60n, 70n, 80n]);
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
test('case 4: start=25, checkpoint=55 → spans two docs', async () => {
|
|
1767
|
+
const ops = await getFilteredOps(25, 55);
|
|
1768
|
+
expect(ops).toEqual([30n, 40n, 50n]);
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
test('case 5: start=35, checkpoint=45 → single op within doc', async () => {
|
|
1772
|
+
const ops = await getFilteredOps(35, 45);
|
|
1773
|
+
expect(ops).toEqual([40n]);
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
test('case 6: start=35, checkpoint=65 → full doc B', async () => {
|
|
1777
|
+
const ops = await getFilteredOps(35, 65);
|
|
1778
|
+
expect(ops).toEqual([40n, 50n, 60n]);
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
test('case 7: start=25, checkpoint=35 → single op from doc A', async () => {
|
|
1782
|
+
const ops = await getFilteredOps(25, 35);
|
|
1783
|
+
expect(ops).toEqual([30n]);
|
|
1784
|
+
});
|
|
1785
|
+
|
|
1786
|
+
test('case 8: start=30, checkpoint=40 → op at checkpoint from next doc', async () => {
|
|
1787
|
+
const ops = await getFilteredOps(30, 40);
|
|
1788
|
+
expect(ops).toEqual([40n]);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
test('case 9: start=100, checkpoint=200 → beyond all docs', async () => {
|
|
1792
|
+
const ops = await getFilteredOps(100, 200);
|
|
1793
|
+
expect(ops).toEqual([]);
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
test('case 10: start=0, checkpoint=5 → before all docs', async () => {
|
|
1797
|
+
const ops = await getFilteredOps(0, 5);
|
|
1798
|
+
expect(ops).toEqual([]);
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
test('case 11: start=50, checkpoint=50 → zero-width range', async () => {
|
|
1802
|
+
const ops = await getFilteredOps(50, 50);
|
|
1803
|
+
expect(ops).toEqual([]);
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
test('case 12: start=45, checkpoint=50 → op at checkpoint boundary', async () => {
|
|
1807
|
+
const ops = await getFilteredOps(45, 50);
|
|
1808
|
+
expect(ops).toEqual([50n]);
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
test('case 13: start=50, checkpoint=55 → no ops strictly after start', async () => {
|
|
1812
|
+
const ops = await getFilteredOps(50, 55);
|
|
1813
|
+
expect(ops).toEqual([]);
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
test('all-filtered first batch still returns data behind the batch boundary', async () => {
|
|
1817
|
+
// Documents straddling the requested (start, end] window are matched by the
|
|
1818
|
+
// query, but contribute no rows after filtering. If an entire server batch
|
|
1819
|
+
// (~101 documents) consists of such straddlers, the remaining documents in
|
|
1820
|
+
// the cursor must still be reachable. Storage reports the straddler buckets
|
|
1821
|
+
// as complete via empty chunks, and the caller re-requests the rest.
|
|
1822
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
1823
|
+
const syncRules = await factory.updateSyncRules(
|
|
1824
|
+
updateSyncRulesFromYaml(
|
|
1825
|
+
`
|
|
1826
|
+
bucket_definitions:
|
|
1827
|
+
by_user:
|
|
1828
|
+
parameters: select request.user_id() as user_id
|
|
1829
|
+
data: [select * from test where owner_id = bucket.user_id]
|
|
1830
|
+
`,
|
|
1831
|
+
{ storageVersion }
|
|
1832
|
+
)
|
|
1833
|
+
);
|
|
1834
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorageV3;
|
|
1835
|
+
const db = bucketStorage.db as VersionedPowerSyncMongoV3;
|
|
1836
|
+
|
|
1837
|
+
const start = 5n;
|
|
1838
|
+
const end = 50n;
|
|
1839
|
+
|
|
1840
|
+
// 150 buckets sorted before the data bucket, each with a single document
|
|
1841
|
+
// containing ops at 1 and 100: matched (_id.o=100 > start, min_op=1 <= end),
|
|
1842
|
+
// but no op in (5, 50].
|
|
1843
|
+
const straddlerNames = Array.from({ length: 150 }, (_, i) => `b${`${i}`.padStart(3, '0')}`);
|
|
1844
|
+
const requests = [...straddlerNames, 'zzz'].map((id) =>
|
|
1845
|
+
bucketRequest(syncRules.syncConfigContent[0], `by_user["${id}"]`, start)
|
|
1846
|
+
);
|
|
1847
|
+
const mapping = syncRules.syncConfigContent[0].mapping;
|
|
1848
|
+
const definitionId = mapping.bucketSourceId(requests[0].source);
|
|
1849
|
+
const collection = db.bucketData(syncRules.replicationStreamId, definitionId);
|
|
1850
|
+
const sourceTable = new bson.ObjectId();
|
|
1851
|
+
|
|
1852
|
+
function makeOps(bucket: string, opIds: bigint[]): BucketDataDoc[] {
|
|
1853
|
+
const bucketKey: BucketKey = {
|
|
1854
|
+
replicationStreamId: syncRules.replicationStreamId,
|
|
1855
|
+
definitionId,
|
|
1856
|
+
bucket
|
|
1857
|
+
};
|
|
1858
|
+
return opIds.map((opId) => ({
|
|
1859
|
+
bucketKey,
|
|
1860
|
+
o: opId,
|
|
1861
|
+
op: 'PUT' as const,
|
|
1862
|
+
source_table: sourceTable,
|
|
1863
|
+
source_key: test_utils.rid(`row-${opId}`),
|
|
1864
|
+
table: 'test',
|
|
1865
|
+
row_id: `row-${opId}`,
|
|
1866
|
+
checksum: BigInt(opId) * 10n,
|
|
1867
|
+
data: `{"id":"row-${opId}"}`
|
|
1868
|
+
}));
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const straddlerDocs = requests
|
|
1872
|
+
.slice(0, -1)
|
|
1873
|
+
.map((request) => serializeBucketData(request.bucket, makeOps(request.bucket, [1n, 100n])));
|
|
1874
|
+
const dataBucket = requests[requests.length - 1].bucket;
|
|
1875
|
+
const dataDoc = serializeBucketData(dataBucket, makeOps(dataBucket, [10n]));
|
|
1876
|
+
await collection.insertMany([...straddlerDocs, dataDoc]);
|
|
1877
|
+
|
|
1878
|
+
// Emulate the caller loop in sync.ts / BucketChecksumState: advance bucket
|
|
1879
|
+
// positions from each chunk, drop completed buckets, and re-request while
|
|
1880
|
+
// any chunk reported has_more.
|
|
1881
|
+
const positions = new Map(requests.map((request) => [request.bucket, request.start]));
|
|
1882
|
+
const pending = new Set(positions.keys());
|
|
1883
|
+
const receivedOps: bigint[] = [];
|
|
1884
|
+
let rounds = 0;
|
|
1885
|
+
|
|
1886
|
+
while (rounds < 10) {
|
|
1887
|
+
rounds++;
|
|
1888
|
+
const roundRequests = requests
|
|
1889
|
+
.filter((request) => pending.has(request.bucket))
|
|
1890
|
+
.map((request) => ({ ...request, start: positions.get(request.bucket)! }));
|
|
1891
|
+
const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(end, roundRequests));
|
|
1892
|
+
let anyHasMore = false;
|
|
1893
|
+
for (const { chunkData } of batch) {
|
|
1894
|
+
positions.set(chunkData.bucket, BigInt(chunkData.next_after));
|
|
1895
|
+
if (chunkData.has_more) {
|
|
1896
|
+
anyHasMore = true;
|
|
1897
|
+
} else {
|
|
1898
|
+
pending.delete(chunkData.bucket);
|
|
1899
|
+
}
|
|
1900
|
+
receivedOps.push(...chunkData.data.map((entry) => BigInt(entry.op_id)));
|
|
1901
|
+
}
|
|
1902
|
+
if (!anyHasMore) {
|
|
1903
|
+
break;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// The op behind the all-straddler first batch must be returned.
|
|
1908
|
+
expect(receivedOps).toEqual([10n]);
|
|
1909
|
+
expect(rounds).toBeLessThan(10);
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
903
1912
|
});
|
|
904
1913
|
}
|
|
905
1914
|
});
|