@powersync/service-module-mongodb-storage 0.15.4 → 0.17.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 +69 -0
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +2 -2
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +8 -6
- package/dist/storage/MongoBucketStorage.js +153 -66
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +15 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/CheckpointState.d.ts +20 -0
- package/dist/storage/implementation/CheckpointState.js +31 -0
- package/dist/storage/implementation/CheckpointState.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +48 -35
- package/dist/storage/implementation/MongoBucketBatch.js +118 -379
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
- package/dist/storage/implementation/MongoChecksums.d.ts +29 -17
- package/dist/storage/implementation/MongoChecksums.js +13 -72
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
- package/dist/storage/implementation/MongoCompactor.js +229 -296
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js +67 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +22 -5
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +56 -13
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +61 -32
- package/dist/storage/implementation/MongoSyncBucketStorage.js +85 -523
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncRulesLock.js +19 -13
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
- package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
- package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
- package/dist/storage/implementation/common/PersistedBatch.js +237 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
- package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
- package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +35 -0
- package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
- package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
- package/dist/storage/implementation/db.d.ts +41 -36
- package/dist/storage/implementation/db.js +77 -99
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +79 -66
- package/dist/storage/implementation/models.js +20 -1
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +27 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +407 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +50 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +354 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +25 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
- package/dist/storage/implementation/v1/models.d.ts +45 -0
- package/dist/storage/implementation/v1/models.js +37 -0
- package/dist/storage/implementation/v1/models.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +30 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +463 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +4 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +63 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +508 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +28 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +22 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +74 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +101 -0
- package/dist/storage/implementation/v3/models.js +34 -0
- package/dist/storage/implementation/v3/models.js.map +1 -0
- package/dist/storage/storage-index.d.ts +6 -3
- package/dist/storage/storage-index.js +6 -3
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/util.d.ts +10 -3
- package/dist/utils/util.js +24 -3
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
- package/src/storage/MongoBucketStorage.ts +254 -99
- package/src/storage/implementation/BucketDefinitionMapping.ts +75 -0
- package/src/storage/implementation/CheckpointState.ts +59 -0
- package/src/storage/implementation/MongoBucketBatch.ts +182 -490
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +53 -75
- package/src/storage/implementation/MongoCompactor.ts +374 -404
- package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
- package/src/storage/implementation/MongoPersistedSyncRules.ts +82 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +78 -16
- package/src/storage/implementation/MongoSyncBucketStorage.ts +179 -628
- package/src/storage/implementation/MongoSyncRulesLock.ts +20 -16
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
- package/src/storage/implementation/OperationBatch.ts +1 -1
- package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
- package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
- package/src/storage/implementation/common/PersistedBatch.ts +364 -0
- package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +48 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +110 -131
- package/src/storage/implementation/models.ts +102 -79
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +509 -0
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +543 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +229 -0
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
- package/src/storage/implementation/v1/models.ts +99 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +607 -0
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
- package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +11 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +678 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +317 -0
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +117 -0
- package/src/storage/implementation/v3/models.ts +164 -0
- package/src/storage/storage-index.ts +6 -3
- package/src/utils/util.ts +34 -5
- package/test/src/storage_compacting.test.ts +57 -29
- package/test/src/storage_sync.test.ts +767 -5
- package/test/src/storeCurrentData.test.ts +211 -0
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
- package/dist/storage/implementation/PersistedBatch.js +0 -354
- package/dist/storage/implementation/PersistedBatch.js.map +0 -1
- package/src/storage/implementation/PersistedBatch.ts +0 -432
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
1
|
+
import { deserializeParameterLookup, JwtPayload, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
2
2
|
import { bucketRequest, register, test_utils } from '@powersync/service-core-tests';
|
|
3
|
+
import { DEFAULT_HYDRATION_STATE, nodeSqlite, RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
4
|
+
import * as bson from 'bson';
|
|
5
|
+
import * as sqlite from 'node:sqlite';
|
|
3
6
|
import { describe, expect, test } from 'vitest';
|
|
7
|
+
import { MongoBucketStorage } from '../../src/storage/MongoBucketStorage.js';
|
|
8
|
+
import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
|
|
9
|
+
import { SourceRecordStoreV3 } from '../../src/storage/implementation/v3/SourceRecordStoreV3.js';
|
|
10
|
+
import type { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
|
|
11
|
+
import {
|
|
12
|
+
CurrentBucketV3,
|
|
13
|
+
ReplicationStreamDocumentV3,
|
|
14
|
+
SyncConfigDefinition
|
|
15
|
+
} from '../../src/storage/implementation/v3/models.js';
|
|
4
16
|
import { INITIALIZED_MONGO_STORAGE_FACTORY, TEST_STORAGE_VERSIONS } from './util.js';
|
|
5
17
|
|
|
18
|
+
const MINIMAL_SYNC_RULES = `
|
|
19
|
+
bucket_definitions:
|
|
20
|
+
global:
|
|
21
|
+
data:
|
|
22
|
+
- SELECT id FROM test
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
function sourceDescriptor(
|
|
26
|
+
name: string,
|
|
27
|
+
options: {
|
|
28
|
+
objectId?: string;
|
|
29
|
+
replicaIdColumns?: string[];
|
|
30
|
+
} = {}
|
|
31
|
+
): storage.SourceEntityDescriptor {
|
|
32
|
+
return {
|
|
33
|
+
connectionTag: storage.SourceTable.DEFAULT_TAG,
|
|
34
|
+
objectId: options.objectId ?? name,
|
|
35
|
+
schema: 'public',
|
|
36
|
+
name,
|
|
37
|
+
replicaIdColumns: (options.replicaIdColumns ?? ['id']).map((column) => ({
|
|
38
|
+
name: column,
|
|
39
|
+
type: 'VARCHAR',
|
|
40
|
+
typeId: 25
|
|
41
|
+
}))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function objectIdGenerator(id: string) {
|
|
46
|
+
let used = false;
|
|
47
|
+
return () => {
|
|
48
|
+
if (used) {
|
|
49
|
+
throw new Error(`Can only generate a single id using ${id}`);
|
|
50
|
+
}
|
|
51
|
+
used = true;
|
|
52
|
+
return new bson.ObjectId(id);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hydratedRulesFor(yaml: string) {
|
|
57
|
+
const parsed = SqlSyncRules.fromYaml(yaml, test_utils.PARSE_OPTIONS);
|
|
58
|
+
expect(parsed.errors).toEqual([]);
|
|
59
|
+
return parsed.config.hydrate({ hydrationState: DEFAULT_HYDRATION_STATE, sqlite: nodeSqlite(sqlite) });
|
|
60
|
+
}
|
|
61
|
+
|
|
6
62
|
function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, storageVersion: number) {
|
|
7
63
|
register.registerSyncTests(storageConfig.factory, {
|
|
8
64
|
storageVersion,
|
|
@@ -126,15 +182,721 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
|
|
|
126
182
|
|
|
127
183
|
// Test that the checksum type is correct.
|
|
128
184
|
// Specifically, test that it never persisted as double.
|
|
129
|
-
const mongoFactory = factory as
|
|
130
|
-
const checksumTypes =
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
186
|
+
const checksumTypes =
|
|
187
|
+
storageVersion >= 3
|
|
188
|
+
? (
|
|
189
|
+
await Promise.all(
|
|
190
|
+
(
|
|
191
|
+
await mongoFactory.db.db
|
|
192
|
+
.listCollections({ name: new RegExp(`^bucket_data_${syncRules.id}_`) }, { nameOnly: true })
|
|
193
|
+
.toArray()
|
|
194
|
+
).map((collection: { name: string }) =>
|
|
195
|
+
mongoFactory.db.db
|
|
196
|
+
.collection(collection.name)
|
|
197
|
+
.aggregate([{ $group: { _id: { $type: '$checksum' }, count: { $sum: 1 } } }])
|
|
198
|
+
.toArray()
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
).flat()
|
|
202
|
+
: await mongoFactory.db.bucket_data
|
|
203
|
+
.aggregate([{ $group: { _id: { $type: '$checksum' }, count: { $sum: 1 } } }])
|
|
204
|
+
.toArray();
|
|
133
205
|
expect(checksumTypes).toEqual([{ _id: 'long', count: 4 }]);
|
|
134
206
|
});
|
|
207
|
+
|
|
208
|
+
test('resolveTables populates matching data and parameter sources', async () => {
|
|
209
|
+
await using factory = await storageConfig.factory();
|
|
210
|
+
const syncRules = await factory.updateSyncRules(
|
|
211
|
+
updateSyncRulesFromYaml(
|
|
212
|
+
`
|
|
213
|
+
bucket_definitions:
|
|
214
|
+
by_owner:
|
|
215
|
+
parameters:
|
|
216
|
+
- SELECT owner_id FROM test WHERE id = token_parameters.test_id
|
|
217
|
+
data:
|
|
218
|
+
- SELECT id, owner_id FROM test WHERE owner_id = bucket.owner_id
|
|
219
|
+
`,
|
|
220
|
+
{ storageVersion }
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
224
|
+
|
|
225
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
226
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
227
|
+
|
|
228
|
+
expect(sourceTable.bucketDataSources).toHaveLength(1);
|
|
229
|
+
expect(sourceTable.parameterLookupSources).toHaveLength(1);
|
|
230
|
+
expect(sourceTable.syncData).toBe(true);
|
|
231
|
+
expect(sourceTable.syncParameters).toBe(true);
|
|
232
|
+
expect(sourceTable.syncEvent).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('resolveTables drops old table when table name changes for the same objectId', async () => {
|
|
236
|
+
await using factory = await storageConfig.factory();
|
|
237
|
+
const syncRules = await factory.updateSyncRules(
|
|
238
|
+
updateSyncRulesFromYaml(
|
|
239
|
+
`
|
|
240
|
+
bucket_definitions:
|
|
241
|
+
global:
|
|
242
|
+
data:
|
|
243
|
+
- SELECT id FROM "%"
|
|
244
|
+
`,
|
|
245
|
+
{ storageVersion }
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
249
|
+
|
|
250
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
251
|
+
const before = await writer.resolveTables({
|
|
252
|
+
connection_id: 1,
|
|
253
|
+
source: sourceDescriptor('orders', { objectId: 'orders-relation' }),
|
|
254
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38342')
|
|
255
|
+
});
|
|
256
|
+
const after = await writer.resolveTables({
|
|
257
|
+
connection_id: 1,
|
|
258
|
+
source: sourceDescriptor('renamed_orders', { objectId: 'orders-relation' }),
|
|
259
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38343')
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(after.tables).toHaveLength(1);
|
|
263
|
+
expect(after.tables[0].id).not.toEqual(before.tables[0].id);
|
|
264
|
+
expect(after.tables[0].bucketDataSources).toHaveLength(1);
|
|
265
|
+
expect(after.tables[0].parameterLookupSources).toHaveLength(0);
|
|
266
|
+
expect(after.dropTables.map((table) => ({ id: table.id, name: table.name }))).toEqual([
|
|
267
|
+
{ id: before.tables[0].id, name: 'orders' }
|
|
268
|
+
]);
|
|
269
|
+
expect(after.dropTables[0].bucketDataSources).toHaveLength(1);
|
|
270
|
+
expect(after.dropTables[0].parameterLookupSources).toHaveLength(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('resolveTables drops old table when objectId changes for the same table name', async () => {
|
|
274
|
+
await using factory = await storageConfig.factory();
|
|
275
|
+
const syncRules = await factory.updateSyncRules(
|
|
276
|
+
updateSyncRulesFromYaml(
|
|
277
|
+
`
|
|
278
|
+
bucket_definitions:
|
|
279
|
+
global:
|
|
280
|
+
data:
|
|
281
|
+
- SELECT id FROM "%"
|
|
282
|
+
`,
|
|
283
|
+
{ storageVersion }
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
287
|
+
|
|
288
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
289
|
+
const before = await writer.resolveTables({
|
|
290
|
+
connection_id: 1,
|
|
291
|
+
source: sourceDescriptor('accounts', { objectId: 'accounts-relation-old' }),
|
|
292
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38344')
|
|
293
|
+
});
|
|
294
|
+
const after = await writer.resolveTables({
|
|
295
|
+
connection_id: 1,
|
|
296
|
+
source: sourceDescriptor('accounts', { objectId: 'accounts-relation-new' }),
|
|
297
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38345')
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(after.tables).toHaveLength(1);
|
|
301
|
+
expect(after.tables[0].id).not.toEqual(before.tables[0].id);
|
|
302
|
+
expect(after.tables[0].bucketDataSources).toHaveLength(1);
|
|
303
|
+
expect(after.dropTables.map((table) => ({ id: table.id, objectId: table.objectId }))).toEqual([
|
|
304
|
+
{ id: before.tables[0].id, objectId: 'accounts-relation-old' }
|
|
305
|
+
]);
|
|
306
|
+
expect(after.dropTables[0].bucketDataSources).toHaveLength(1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('resolveTables drops old table when replica id columns change', async () => {
|
|
310
|
+
await using factory = await storageConfig.factory();
|
|
311
|
+
const syncRules = await factory.updateSyncRules(
|
|
312
|
+
updateSyncRulesFromYaml(
|
|
313
|
+
`
|
|
314
|
+
bucket_definitions:
|
|
315
|
+
global:
|
|
316
|
+
data:
|
|
317
|
+
- SELECT id FROM "%"
|
|
318
|
+
`,
|
|
319
|
+
{ storageVersion }
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
323
|
+
|
|
324
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
325
|
+
const before = await writer.resolveTables({
|
|
326
|
+
connection_id: 1,
|
|
327
|
+
source: sourceDescriptor('items', { objectId: 'items-relation', replicaIdColumns: ['id'] }),
|
|
328
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38346')
|
|
329
|
+
});
|
|
330
|
+
const after = await writer.resolveTables({
|
|
331
|
+
connection_id: 1,
|
|
332
|
+
source: sourceDescriptor('items', { objectId: 'items-relation', replicaIdColumns: ['tenant_id', 'id'] }),
|
|
333
|
+
idGenerator: objectIdGenerator('6544e3899293153fa7b38347')
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(after.tables).toHaveLength(1);
|
|
337
|
+
expect(after.tables[0].id).not.toEqual(before.tables[0].id);
|
|
338
|
+
expect(after.tables[0].replicaIdColumns.map((column) => column.name)).toEqual(['tenant_id', 'id']);
|
|
339
|
+
expect(after.tables[0].bucketDataSources).toHaveLength(1);
|
|
340
|
+
expect(
|
|
341
|
+
after.dropTables.map((table) => ({ id: table.id, columns: table.replicaIdColumns.map((c) => c.name) }))
|
|
342
|
+
).toEqual([{ id: before.tables[0].id, columns: ['id'] }]);
|
|
343
|
+
expect(after.dropTables[0].bucketDataSources).toHaveLength(1);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test.runIf(storageVersion >= 3)(
|
|
347
|
+
'resolveTables resolves v3 event-only tables without source memberships',
|
|
348
|
+
async () => {
|
|
349
|
+
await using factory = await storageConfig.factory();
|
|
350
|
+
const syncRules = await factory.updateSyncRules(
|
|
351
|
+
updateSyncRulesFromYaml(
|
|
352
|
+
`
|
|
353
|
+
bucket_definitions:
|
|
354
|
+
by_owner:
|
|
355
|
+
data:
|
|
356
|
+
- SELECT id FROM users
|
|
357
|
+
|
|
358
|
+
event_definitions:
|
|
359
|
+
write_checkpoints:
|
|
360
|
+
payloads:
|
|
361
|
+
- SELECT user_id, checkpoint FROM checkpoints
|
|
362
|
+
`,
|
|
363
|
+
{ storageVersion }
|
|
364
|
+
)
|
|
365
|
+
);
|
|
366
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
367
|
+
|
|
368
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
369
|
+
const resolved = await writer.resolveTables({
|
|
370
|
+
connection_id: 1,
|
|
371
|
+
source: {
|
|
372
|
+
connectionTag: storage.SourceTable.DEFAULT_TAG,
|
|
373
|
+
objectId: 'checkpoints',
|
|
374
|
+
schema: 'public',
|
|
375
|
+
name: 'checkpoints',
|
|
376
|
+
replicaIdColumns: [{ name: 'id', type: 'VARCHAR', typeId: 25 }]
|
|
377
|
+
},
|
|
378
|
+
idGenerator: () => new bson.ObjectId('6544e3899293153fa7b38341')
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(resolved.tables).toHaveLength(1);
|
|
382
|
+
expect(resolved.dropTables).toHaveLength(0);
|
|
383
|
+
expect(resolved.tables[0].bucketDataSources).toEqual([]);
|
|
384
|
+
expect(resolved.tables[0].parameterLookupSources).toEqual([]);
|
|
385
|
+
expect(resolved.tables[0].syncData).toBe(false);
|
|
386
|
+
expect(resolved.tables[0].syncParameters).toBe(false);
|
|
387
|
+
expect(resolved.tables[0].syncEvent).toBe(true);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
test.runIf(storageVersion >= 3)('resolveTables handles v3 source membership additions and removals', async () => {
|
|
392
|
+
// Tests the behavior of resolveTables when bucket data sources and parameter index creators are added or removed.
|
|
393
|
+
// These are not end-to-end tests yet, since we don't have a full incremental reprocessing implementation.
|
|
394
|
+
// This just tests the specific resolveTables behavior.
|
|
395
|
+
|
|
396
|
+
// The same tests should work with sync streams, but legacy bucket_definitions make it easy
|
|
397
|
+
// to see the distinction between the parameter index queries and the data sources.
|
|
398
|
+
const fullRulesYaml = `
|
|
399
|
+
bucket_definitions:
|
|
400
|
+
by_owner:
|
|
401
|
+
parameters:
|
|
402
|
+
- SELECT owner_id FROM memberships WHERE id = token_parameters.test_id
|
|
403
|
+
data:
|
|
404
|
+
- SELECT id, owner_id FROM memberships WHERE owner_id = bucket.owner_id
|
|
405
|
+
`;
|
|
406
|
+
const dataOnlyRulesYaml = `
|
|
407
|
+
bucket_definitions:
|
|
408
|
+
by_owner:
|
|
409
|
+
parameters:
|
|
410
|
+
- SELECT token_parameters.owner_id as owner_id
|
|
411
|
+
data:
|
|
412
|
+
- SELECT id, owner_id FROM memberships WHERE owner_id = bucket.owner_id
|
|
413
|
+
`;
|
|
414
|
+
const parameterOnlyRulesYaml = `
|
|
415
|
+
bucket_definitions:
|
|
416
|
+
by_owner:
|
|
417
|
+
parameters:
|
|
418
|
+
- SELECT owner_id FROM memberships WHERE id = token_parameters.test_id
|
|
419
|
+
data: []
|
|
420
|
+
`;
|
|
421
|
+
const eventOnlyRulesYaml = `
|
|
422
|
+
bucket_definitions: {}
|
|
423
|
+
|
|
424
|
+
event_definitions:
|
|
425
|
+
write_checkpoints:
|
|
426
|
+
payloads:
|
|
427
|
+
- SELECT id, owner_id FROM memberships
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
await using factory = await storageConfig.factory();
|
|
431
|
+
// This does not quite match what actual API usage would look like.
|
|
432
|
+
// Here we're persisting one sync config, then resolving tables with others.
|
|
433
|
+
// We're also using the default hydration state for them all.
|
|
434
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(fullRulesYaml, { storageVersion }));
|
|
435
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
436
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
437
|
+
const fullRules = hydratedRulesFor(fullRulesYaml);
|
|
438
|
+
const dataOnlyRules = hydratedRulesFor(dataOnlyRulesYaml);
|
|
439
|
+
const parameterOnlyRules = hydratedRulesFor(parameterOnlyRulesYaml);
|
|
440
|
+
const eventOnlyRules = hydratedRulesFor(eventOnlyRulesYaml);
|
|
441
|
+
const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
|
|
442
|
+
const dataOnlyTableId = new bson.ObjectId('6544e3899293153fa7b38348');
|
|
443
|
+
const addedParameterTableId = new bson.ObjectId('6544e3899293153fa7b38349');
|
|
444
|
+
const removedDataTableId = new bson.ObjectId('6544e3899293153fa7b3834a');
|
|
445
|
+
|
|
446
|
+
const dataOnly = await writer.resolveTables({
|
|
447
|
+
connection_id: 1,
|
|
448
|
+
source,
|
|
449
|
+
idGenerator: () => dataOnlyTableId,
|
|
450
|
+
syncRules: dataOnlyRules
|
|
451
|
+
});
|
|
452
|
+
expect(dataOnly.tables.map((table) => table.id)).toEqual([dataOnlyTableId]);
|
|
453
|
+
expect(dataOnly.dropTables.map((table) => table.id)).toEqual([]);
|
|
454
|
+
expect(dataOnly.tables[0].bucketDataSources).toHaveLength(1);
|
|
455
|
+
expect(dataOnly.tables[0].parameterLookupSources).toHaveLength(0);
|
|
456
|
+
|
|
457
|
+
const addedParameter = await writer.resolveTables({
|
|
458
|
+
connection_id: 1,
|
|
459
|
+
source,
|
|
460
|
+
idGenerator: () => addedParameterTableId,
|
|
461
|
+
syncRules: fullRules
|
|
462
|
+
});
|
|
463
|
+
// Adding a definition always creates a new SourceTable
|
|
464
|
+
expect(addedParameter.tables.map((table) => table.id)).toEqual([dataOnlyTableId, addedParameterTableId]);
|
|
465
|
+
expect(addedParameter.tables.map((table) => table.bucketDataSources.length).sort()).toEqual([0, 1]);
|
|
466
|
+
expect(addedParameter.tables.map((table) => table.parameterLookupSources.length).sort()).toEqual([0, 1]);
|
|
467
|
+
expect(addedParameter.dropTables.map((table) => table.id)).toEqual([]);
|
|
468
|
+
|
|
469
|
+
const removedParameter = await writer.resolveTables({
|
|
470
|
+
connection_id: 1,
|
|
471
|
+
source,
|
|
472
|
+
idGenerator: () => {
|
|
473
|
+
throw new Error('data-only resolve should reuse existing v3 source table');
|
|
474
|
+
},
|
|
475
|
+
syncRules: dataOnlyRules
|
|
476
|
+
});
|
|
477
|
+
expect(removedParameter.tables.map((table) => table.id)).toEqual([dataOnlyTableId]);
|
|
478
|
+
// Now this sourceTable is unused & dropped
|
|
479
|
+
expect(removedParameter.dropTables.map((table) => table.id)).toEqual([addedParameterTableId]);
|
|
480
|
+
expect(removedParameter.tables[0].bucketDataSources).toHaveLength(1);
|
|
481
|
+
expect(removedParameter.tables[0].parameterLookupSources).toHaveLength(0);
|
|
482
|
+
await writer.drop(removedParameter.dropTables);
|
|
483
|
+
|
|
484
|
+
const removedData = await writer.resolveTables({
|
|
485
|
+
connection_id: 1,
|
|
486
|
+
source,
|
|
487
|
+
idGenerator: () => removedDataTableId,
|
|
488
|
+
syncRules: parameterOnlyRules
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// This goes from dataOnlyRules -> parameterOnlyRules, which adds one definition and removes another.
|
|
492
|
+
// This generates a new SourceTable again, and removes all others.
|
|
493
|
+
expect(removedData.tables.map((table) => table.id)).toEqual([removedDataTableId]);
|
|
494
|
+
expect(removedData.dropTables.map((table) => table.id)).toEqual([dataOnlyTableId]);
|
|
495
|
+
expect(removedData.tables[0].bucketDataSources).toHaveLength(0);
|
|
496
|
+
expect(removedData.tables[0].parameterLookupSources).toHaveLength(1);
|
|
497
|
+
await writer.drop(removedData.dropTables);
|
|
498
|
+
|
|
499
|
+
const eventOnly = await writer.resolveTables({
|
|
500
|
+
connection_id: 1,
|
|
501
|
+
source,
|
|
502
|
+
idGenerator: () => {
|
|
503
|
+
throw new Error('resolve should reuse existing v3 source table');
|
|
504
|
+
},
|
|
505
|
+
syncRules: eventOnlyRules
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Event-only table can re-use any existing table.
|
|
509
|
+
expect(eventOnly.tables.map((table) => table.id)).toEqual([removedDataTableId]);
|
|
510
|
+
expect(eventOnly.dropTables.map((table) => table.id)).toEqual([]);
|
|
511
|
+
expect(eventOnly.tables[0].bucketDataSources).toHaveLength(0);
|
|
512
|
+
expect(eventOnly.tables[0].parameterLookupSources).toHaveLength(0);
|
|
513
|
+
expect(eventOnly.tables[0].syncData).toBe(false);
|
|
514
|
+
expect(eventOnly.tables[0].syncParameters).toBe(false);
|
|
515
|
+
expect(eventOnly.tables[0].syncEvent).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test.runIf(storageVersion >= 3)('uses v3 mongodb model shapes', async () => {
|
|
519
|
+
await using factory = await storageConfig.factory();
|
|
520
|
+
const syncRules = await factory.updateSyncRules(
|
|
521
|
+
updateSyncRulesFromYaml(
|
|
522
|
+
`
|
|
523
|
+
bucket_definitions:
|
|
524
|
+
global:
|
|
525
|
+
parameters:
|
|
526
|
+
- SELECT owner_id FROM test WHERE id = token_parameters.test
|
|
527
|
+
data:
|
|
528
|
+
- SELECT id, description, owner_id FROM test WHERE id = bucket.owner_id
|
|
529
|
+
`,
|
|
530
|
+
{ storageVersion }
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
534
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
535
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
536
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
537
|
+
|
|
538
|
+
await writer.save({
|
|
539
|
+
sourceTable,
|
|
540
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
541
|
+
after: {
|
|
542
|
+
id: 'shape-check',
|
|
543
|
+
description: 'shape',
|
|
544
|
+
owner_id: 'user-1'
|
|
545
|
+
},
|
|
546
|
+
afterReplicaId: test_utils.rid('shape-check')
|
|
547
|
+
});
|
|
548
|
+
await writer.markAllSnapshotDone('1/1');
|
|
549
|
+
await writer.commit('1/1');
|
|
550
|
+
|
|
551
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
552
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: { test: 'shape-check' } }), {});
|
|
553
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
554
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
555
|
+
async getParameterSets(lookups) {
|
|
556
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['shape-check']]);
|
|
557
|
+
expect(lookups[0].indexId).toEqual('1');
|
|
558
|
+
|
|
559
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
560
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ owner_id: 'user-1' }] }]);
|
|
561
|
+
return parameter_sets;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'global["user-1"]').bucket]);
|
|
565
|
+
|
|
566
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
567
|
+
const db = (bucketStorage as MongoSyncBucketStorage).db as VersionedPowerSyncMongoV3;
|
|
568
|
+
const currentDataCollections = await db.listSourceRecordCollectionsV3(syncRules.id);
|
|
569
|
+
const currentData = await currentDataCollections[0]?.findOne({});
|
|
570
|
+
const firstBucket: CurrentBucketV3 | undefined = currentData?.buckets[0] as CurrentBucketV3 | undefined;
|
|
571
|
+
expect(firstBucket?.def).toMatch(/^[0-9a-f]+$/);
|
|
572
|
+
|
|
573
|
+
const bucketCollections = await mongoFactory.db.db
|
|
574
|
+
.listCollections({ name: new RegExp(`^bucket_data_${syncRules.id}_`) }, { nameOnly: true })
|
|
575
|
+
.toArray();
|
|
576
|
+
expect(
|
|
577
|
+
bucketCollections.some((collection) => collection.name === `bucket_data_${syncRules.id}_${firstBucket?.def}`)
|
|
578
|
+
).toBe(true);
|
|
579
|
+
|
|
580
|
+
const syncRule = (await mongoFactory.db.sync_rules.findOne({ _id: syncRules.id })) as ReplicationStreamDocumentV3;
|
|
581
|
+
const syncConfig = await db.syncConfigDefinitions.findOne({ _id: syncRule.sync_configs[0]._id });
|
|
582
|
+
const ruleMapping: SyncConfigDefinition['rule_mapping'] | undefined = syncConfig?.rule_mapping;
|
|
583
|
+
expect(Object.keys(ruleMapping?.definitions ?? {})).not.toHaveLength(0);
|
|
584
|
+
|
|
585
|
+
const parameterIndexId = Object.values(ruleMapping?.parameter_indexes ?? {})[0] as string | undefined;
|
|
586
|
+
expect(parameterIndexId).toBeDefined();
|
|
587
|
+
const parameterEntry = await db.parameterIndexV3(syncRules.id, parameterIndexId!).findOne({});
|
|
588
|
+
expect(deserializeParameterLookup(parameterEntry!.lookup)).toEqual(['shape-check']);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test.runIf(storageVersion < 3)('can replace processing legacy sync rules', async () => {
|
|
592
|
+
await using factory = await storageConfig.factory();
|
|
593
|
+
|
|
594
|
+
const firstSyncRules = await factory.updateSyncRules(
|
|
595
|
+
updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion })
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
await expect(
|
|
599
|
+
factory.updateSyncRules(updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion }))
|
|
600
|
+
).resolves.toBeDefined();
|
|
601
|
+
|
|
602
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
603
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: firstSyncRules.id }))?.state).toBe(
|
|
604
|
+
storage.SyncRuleState.STOP
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test('can lock newly-created sync rules', async () => {
|
|
609
|
+
await using factory = await storageConfig.factory();
|
|
610
|
+
|
|
611
|
+
const syncRules = await factory.updateSyncRules(
|
|
612
|
+
updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion, lock: true })
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
expect(syncRules.current_lock?.sync_rules_id).toBe(syncRules.id);
|
|
616
|
+
await syncRules.current_lock?.release();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test.runIf(storageVersion < 3)('uses a single current_data collection for v1 source records', async () => {
|
|
620
|
+
await using factory = await storageConfig.factory();
|
|
621
|
+
const syncRules = await factory.updateSyncRules(
|
|
622
|
+
updateSyncRulesFromYaml(
|
|
623
|
+
`
|
|
624
|
+
bucket_definitions:
|
|
625
|
+
global:
|
|
626
|
+
data:
|
|
627
|
+
- SELECT id, description FROM test
|
|
628
|
+
`,
|
|
629
|
+
{ storageVersion }
|
|
630
|
+
)
|
|
631
|
+
);
|
|
632
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
633
|
+
|
|
634
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
635
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
636
|
+
|
|
637
|
+
await writer.save({
|
|
638
|
+
sourceTable,
|
|
639
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
640
|
+
after: {
|
|
641
|
+
id: 'shape-check',
|
|
642
|
+
description: 'shape'
|
|
643
|
+
},
|
|
644
|
+
afterReplicaId: test_utils.rid('shape-check')
|
|
645
|
+
});
|
|
646
|
+
await writer.markAllSnapshotDone('1/1');
|
|
647
|
+
await writer.commit('1/1');
|
|
648
|
+
|
|
649
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
650
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.id })).toBe(1);
|
|
651
|
+
|
|
652
|
+
const sourceRecordCollections = await mongoFactory.db.db
|
|
653
|
+
.listCollections({ name: new RegExp(`^source_records_${syncRules.id}_`) }, { nameOnly: true })
|
|
654
|
+
.toArray();
|
|
655
|
+
expect(sourceRecordCollections).toEqual([]);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test.runIf(storageVersion < 3)('clear removes v1 current_data rows', async () => {
|
|
659
|
+
await using factory = await storageConfig.factory();
|
|
660
|
+
const syncRules = await factory.updateSyncRules(
|
|
661
|
+
updateSyncRulesFromYaml(
|
|
662
|
+
`
|
|
663
|
+
bucket_definitions:
|
|
664
|
+
global:
|
|
665
|
+
data:
|
|
666
|
+
- SELECT id, description FROM test
|
|
667
|
+
`,
|
|
668
|
+
{ storageVersion }
|
|
669
|
+
)
|
|
670
|
+
);
|
|
671
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
672
|
+
|
|
673
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
674
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
675
|
+
|
|
676
|
+
await writer.save({
|
|
677
|
+
sourceTable,
|
|
678
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
679
|
+
after: {
|
|
680
|
+
id: 'clear-check',
|
|
681
|
+
description: 'shape'
|
|
682
|
+
},
|
|
683
|
+
afterReplicaId: test_utils.rid('clear-check')
|
|
684
|
+
});
|
|
685
|
+
await writer.markAllSnapshotDone('1/1');
|
|
686
|
+
await writer.commit('1/1');
|
|
687
|
+
|
|
688
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
689
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.id })).toBe(1);
|
|
690
|
+
|
|
691
|
+
await bucketStorage.clear();
|
|
692
|
+
|
|
693
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.id })).toBe(0);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test.runIf(storageVersion < 3)('storage metrics include v1 current_data', async () => {
|
|
697
|
+
await using factory = await storageConfig.factory();
|
|
698
|
+
const syncRules = await factory.updateSyncRules(
|
|
699
|
+
updateSyncRulesFromYaml(
|
|
700
|
+
`
|
|
701
|
+
bucket_definitions:
|
|
702
|
+
global:
|
|
703
|
+
data:
|
|
704
|
+
- SELECT id, description FROM test
|
|
705
|
+
`,
|
|
706
|
+
{ storageVersion }
|
|
707
|
+
)
|
|
708
|
+
);
|
|
709
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
710
|
+
const metricsBefore = await factory.getStorageMetrics();
|
|
711
|
+
|
|
712
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
713
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
714
|
+
|
|
715
|
+
await writer.save({
|
|
716
|
+
sourceTable,
|
|
717
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
718
|
+
after: {
|
|
719
|
+
id: 'metric-check',
|
|
720
|
+
description: 'shape'
|
|
721
|
+
},
|
|
722
|
+
afterReplicaId: test_utils.rid('metric-check')
|
|
723
|
+
});
|
|
724
|
+
await writer.markAllSnapshotDone('1/1');
|
|
725
|
+
await writer.commit('1/1');
|
|
726
|
+
|
|
727
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
728
|
+
expect(await mongoFactory.db.current_data.countDocuments({ '_id.g': syncRules.id })).toBe(1);
|
|
729
|
+
|
|
730
|
+
const metricsAfter = await factory.getStorageMetrics();
|
|
731
|
+
expect(metricsAfter.replication_size_bytes).toBeGreaterThan(metricsBefore.replication_size_bytes);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test.runIf(storageVersion >= 3)(
|
|
735
|
+
'loads parameter checkpoint changes across all v3 parameter index collections',
|
|
736
|
+
async () => {
|
|
737
|
+
await using factory = await storageConfig.factory();
|
|
738
|
+
const syncRules = await factory.updateSyncRules(
|
|
739
|
+
updateSyncRulesFromYaml(
|
|
740
|
+
`
|
|
741
|
+
bucket_definitions:
|
|
742
|
+
by_owner:
|
|
743
|
+
parameters:
|
|
744
|
+
- SELECT owner_id FROM test WHERE id = token_parameters.owner_lookup
|
|
745
|
+
data:
|
|
746
|
+
- SELECT id, owner_id FROM test WHERE owner_id = bucket.owner_id
|
|
747
|
+
by_category:
|
|
748
|
+
parameters:
|
|
749
|
+
- SELECT category_id FROM test WHERE id = token_parameters.category_lookup
|
|
750
|
+
data:
|
|
751
|
+
- SELECT id, category_id FROM test WHERE category_id = bucket.category_id
|
|
752
|
+
`,
|
|
753
|
+
{ storageVersion }
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
|
|
757
|
+
const previousCheckpoint = await bucketStorage.getCheckpoint();
|
|
758
|
+
|
|
759
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
760
|
+
const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], INITIALIZED_MONGO_STORAGE_FACTORY);
|
|
761
|
+
|
|
762
|
+
await writer.save({
|
|
763
|
+
sourceTable,
|
|
764
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
765
|
+
after: {
|
|
766
|
+
id: 'shape-check',
|
|
767
|
+
owner_id: 'user-1',
|
|
768
|
+
category_id: 'cat-1'
|
|
769
|
+
},
|
|
770
|
+
afterReplicaId: test_utils.rid('shape-check')
|
|
771
|
+
});
|
|
772
|
+
await writer.markAllSnapshotDone('1/1');
|
|
773
|
+
await writer.commit('1/1');
|
|
774
|
+
|
|
775
|
+
const nextCheckpoint = await bucketStorage.getCheckpoint();
|
|
776
|
+
const changes = await bucketStorage.getCheckpointChanges({
|
|
777
|
+
lastCheckpoint: previousCheckpoint,
|
|
778
|
+
nextCheckpoint
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(changes.invalidateParameterBuckets).toBe(false);
|
|
782
|
+
expect(changes.updatedParameterLookups).toEqual(new Set(['["1","","shape-check"]', '["2","","shape-check"]']));
|
|
783
|
+
}
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
test.runIf(storageVersion >= 3)('cleans pending deletes only for tracked v3 source tables', async () => {
|
|
787
|
+
await using factory = await storageConfig.factory();
|
|
788
|
+
const syncRules = await factory.updateSyncRules(
|
|
789
|
+
updateSyncRulesFromYaml(
|
|
790
|
+
`
|
|
791
|
+
bucket_definitions:
|
|
792
|
+
global:
|
|
793
|
+
data:
|
|
794
|
+
- SELECT id, description FROM test
|
|
795
|
+
`,
|
|
796
|
+
{ storageVersion }
|
|
797
|
+
)
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
801
|
+
const bucketStorage = mongoFactory.getInstance(syncRules) as any;
|
|
802
|
+
const db = bucketStorage.db;
|
|
803
|
+
await db.initializeStreamStorage(syncRules.id);
|
|
804
|
+
|
|
805
|
+
const sourceTableA = new bson.ObjectId();
|
|
806
|
+
const sourceTableB = new bson.ObjectId();
|
|
807
|
+
await db.sourceTablesV3(syncRules.id).insertMany([
|
|
808
|
+
{
|
|
809
|
+
_id: sourceTableA,
|
|
810
|
+
connection_id: 1,
|
|
811
|
+
relation_id: 'a',
|
|
812
|
+
schema_name: 'public',
|
|
813
|
+
table_name: 'table_a',
|
|
814
|
+
replica_id_columns: null,
|
|
815
|
+
replica_id_columns2: [],
|
|
816
|
+
snapshot_done: true,
|
|
817
|
+
snapshot_status: undefined,
|
|
818
|
+
bucket_data_source_ids: [],
|
|
819
|
+
parameter_lookup_source_ids: [],
|
|
820
|
+
latest_pending_delete: 9n
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
_id: sourceTableB,
|
|
824
|
+
connection_id: 1,
|
|
825
|
+
relation_id: 'b',
|
|
826
|
+
schema_name: 'public',
|
|
827
|
+
table_name: 'table_b',
|
|
828
|
+
replica_id_columns: null,
|
|
829
|
+
replica_id_columns2: [],
|
|
830
|
+
snapshot_done: true,
|
|
831
|
+
snapshot_status: undefined,
|
|
832
|
+
bucket_data_source_ids: [],
|
|
833
|
+
parameter_lookup_source_ids: [],
|
|
834
|
+
latest_pending_delete: 12n
|
|
835
|
+
}
|
|
836
|
+
]);
|
|
837
|
+
|
|
838
|
+
await db.sourceRecordsV3(syncRules.id, sourceTableA).insertMany([
|
|
839
|
+
{ _id: 'deleted-1', data: null, buckets: [], lookups: [], pending_delete: 5n },
|
|
840
|
+
{ _id: 'deleted-2', data: null, buckets: [], lookups: [], pending_delete: 9n },
|
|
841
|
+
{ _id: 'active', data: null, buckets: [], lookups: [] }
|
|
842
|
+
]);
|
|
843
|
+
await db
|
|
844
|
+
.sourceRecordsV3(syncRules.id, sourceTableB)
|
|
845
|
+
.insertMany([{ _id: 'later-delete', data: null, buckets: [], lookups: [], pending_delete: 12n }]);
|
|
846
|
+
|
|
847
|
+
const store = new SourceRecordStoreV3(db, syncRules.id, bucketStorage.sync_rules.mapping);
|
|
848
|
+
const logger = { info() {} } as any;
|
|
849
|
+
|
|
850
|
+
await store.postCommitCleanup(6n, logger);
|
|
851
|
+
|
|
852
|
+
expect(await db.sourceRecordsV3(syncRules.id, sourceTableA).countDocuments({ pending_delete: 5n })).toBe(0);
|
|
853
|
+
expect(await db.sourceRecordsV3(syncRules.id, sourceTableA).countDocuments({ pending_delete: 9n })).toBe(1);
|
|
854
|
+
expect(await db.sourceRecordsV3(syncRules.id, sourceTableB).countDocuments({ pending_delete: 12n })).toBe(1);
|
|
855
|
+
expect((await db.sourceTablesV3(syncRules.id).findOne({ _id: sourceTableA }))?.latest_pending_delete).toBe(9n);
|
|
856
|
+
expect((await db.sourceTablesV3(syncRules.id).findOne({ _id: sourceTableB }))?.latest_pending_delete).toBe(12n);
|
|
857
|
+
|
|
858
|
+
await store.postCommitCleanup(10n, logger);
|
|
859
|
+
|
|
860
|
+
expect(
|
|
861
|
+
await db.sourceRecordsV3(syncRules.id, sourceTableA).countDocuments({ pending_delete: { $exists: true } })
|
|
862
|
+
).toBe(0);
|
|
863
|
+
expect(
|
|
864
|
+
(await db.sourceTablesV3(syncRules.id).findOne({ _id: sourceTableA }))?.latest_pending_delete
|
|
865
|
+
).toBeUndefined();
|
|
866
|
+
expect((await db.sourceTablesV3(syncRules.id).findOne({ _id: sourceTableB }))?.latest_pending_delete).toBe(12n);
|
|
867
|
+
});
|
|
135
868
|
}
|
|
136
869
|
|
|
137
870
|
describe('sync - mongodb', () => {
|
|
871
|
+
test('v3 activation stops legacy active sync rules', async () => {
|
|
872
|
+
await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
|
|
873
|
+
const mongoFactory = factory as MongoBucketStorage;
|
|
874
|
+
|
|
875
|
+
const legacySyncRules = await factory.updateSyncRules(
|
|
876
|
+
updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion: storage.LEGACY_STORAGE_VERSION })
|
|
877
|
+
);
|
|
878
|
+
const legacyStorage = factory.getInstance(legacySyncRules);
|
|
879
|
+
await using legacyWriter = await legacyStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
880
|
+
await legacyWriter.markAllSnapshotDone('1/1');
|
|
881
|
+
await legacyWriter.commit('1/1');
|
|
882
|
+
|
|
883
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.id }))?.state).toBe(
|
|
884
|
+
storage.SyncRuleState.ACTIVE
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const v3SyncRules = await factory.updateSyncRules(
|
|
888
|
+
updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion: storage.STORAGE_VERSION_3 })
|
|
889
|
+
);
|
|
890
|
+
const v3Storage = factory.getInstance(v3SyncRules);
|
|
891
|
+
await using v3Writer = await v3Storage.createWriter(test_utils.BATCH_OPTIONS);
|
|
892
|
+
await v3Writer.markAllSnapshotDone('2/1');
|
|
893
|
+
await v3Writer.commit('2/1');
|
|
894
|
+
|
|
895
|
+
expect((await mongoFactory.db.sync_rules.findOne({ _id: legacySyncRules.id }))?.state).toBe(
|
|
896
|
+
storage.SyncRuleState.STOP
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
|
|
138
900
|
for (const storageVersion of TEST_STORAGE_VERSIONS) {
|
|
139
901
|
describe(`storage v${storageVersion}`, () => {
|
|
140
902
|
registerSyncStorageTests(INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion);
|