@powersync/service-core-tests 0.15.4 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/dist/test-utils/AbstractStreamTestContext.d.ts +51 -0
- package/dist/test-utils/AbstractStreamTestContext.js +143 -0
- package/dist/test-utils/AbstractStreamTestContext.js.map +1 -0
- package/dist/test-utils/StorageDataHelpers.d.ts +8 -0
- package/dist/test-utils/StorageDataHelpers.js +33 -0
- package/dist/test-utils/StorageDataHelpers.js.map +1 -0
- package/dist/test-utils/general-utils.d.ts +3 -8
- package/dist/test-utils/general-utils.js +28 -26
- package/dist/test-utils/general-utils.js.map +1 -1
- package/dist/test-utils/test-utils-index.d.ts +2 -0
- package/dist/test-utils/test-utils-index.js +2 -0
- package/dist/test-utils/test-utils-index.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.js +3 -3
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-data-storage-parameter-tests.js +436 -65
- package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
- package/dist/tests/register-parameter-compacting-tests.js +8 -8
- package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
- package/dist/tests/util.d.ts +1 -2
- package/dist/tests/util.js +8 -5
- package/dist/tests/util.js.map +1 -1
- package/package.json +7 -6
- package/src/test-utils/AbstractStreamTestContext.ts +179 -0
- package/src/test-utils/StorageDataHelpers.ts +44 -0
- package/src/test-utils/general-utils.ts +31 -31
- package/src/test-utils/test-utils-index.ts +2 -0
- package/src/tests/register-data-storage-data-tests.ts +3 -3
- package/src/tests/register-data-storage-parameter-tests.ts +519 -67
- package/src/tests/register-parameter-compacting-tests.ts +8 -8
- package/src/tests/util.ts +12 -9
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { CURRENT_STORAGE_VERSION, JwtPayload, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
|
|
2
|
-
import { RequestParameters, ScopedParameterLookup } from '@powersync/service-sync-rules';
|
|
2
|
+
import { RequestParameters, ScopedParameterLookup, UnscopedParameterLookup } from '@powersync/service-sync-rules';
|
|
3
3
|
import { expect, test } from 'vitest';
|
|
4
4
|
import * as test_utils from '../test-utils/test-utils-index.js';
|
|
5
5
|
import { bucketRequest } from '../test-utils/test-utils-index.js';
|
|
6
|
-
import { parameterLookupScope } from './util.js';
|
|
7
6
|
/**
|
|
8
7
|
* @example
|
|
9
8
|
* ```TypeScript
|
|
@@ -17,7 +16,6 @@ import { parameterLookupScope } from './util.js';
|
|
|
17
16
|
export function registerDataStorageParameterTests(config) {
|
|
18
17
|
const generateStorageFactory = config.factory;
|
|
19
18
|
const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
|
|
20
|
-
const MYBUCKET_1 = parameterLookupScope('mybucket', '1');
|
|
21
19
|
test('save and load parameters', async () => {
|
|
22
20
|
await using factory = await generateStorageFactory();
|
|
23
21
|
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
|
|
@@ -30,6 +28,7 @@ bucket_definitions:
|
|
|
30
28
|
storageVersion
|
|
31
29
|
}));
|
|
32
30
|
const bucketStorage = factory.getInstance(syncRules);
|
|
31
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
33
32
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
34
33
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
35
34
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -57,12 +56,17 @@ bucket_definitions:
|
|
|
57
56
|
});
|
|
58
57
|
await writer.commit('1/1');
|
|
59
58
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
60
|
-
const parameters =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
|
|
60
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
61
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
62
|
+
async getParameterSets(lookups) {
|
|
63
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
|
|
64
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
65
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1a' }] }]);
|
|
66
|
+
return parameter_sets;
|
|
64
67
|
}
|
|
65
|
-
|
|
68
|
+
});
|
|
69
|
+
expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1a"]').bucket]);
|
|
66
70
|
});
|
|
67
71
|
test('it should use the latest version', async () => {
|
|
68
72
|
await using factory = await generateStorageFactory();
|
|
@@ -76,6 +80,7 @@ bucket_definitions:
|
|
|
76
80
|
storageVersion
|
|
77
81
|
}));
|
|
78
82
|
const bucketStorage = factory.getInstance(syncRules);
|
|
83
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
79
84
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
80
85
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
81
86
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -101,19 +106,26 @@ bucket_definitions:
|
|
|
101
106
|
});
|
|
102
107
|
await writer.commit('1/2');
|
|
103
108
|
const checkpoint2 = await bucketStorage.getCheckpoint();
|
|
104
|
-
const parameters =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
|
|
110
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
111
|
+
const buckets1 = await querier.queryDynamicBucketDescriptions({
|
|
112
|
+
async getParameterSets(lookups) {
|
|
113
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
|
|
114
|
+
const parameter_sets = await checkpoint1.getParameterSets(lookups, 1000);
|
|
115
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1' }] }]);
|
|
116
|
+
return parameter_sets;
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
});
|
|
119
|
+
expect(buckets1.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1"]').bucket]);
|
|
120
|
+
const buckets2 = await querier.queryDynamicBucketDescriptions({
|
|
121
|
+
async getParameterSets(lookups) {
|
|
122
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
|
|
123
|
+
const parameter_sets = await checkpoint2.getParameterSets(lookups, 1000);
|
|
124
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group2' }] }]);
|
|
125
|
+
return parameter_sets;
|
|
115
126
|
}
|
|
116
|
-
|
|
127
|
+
});
|
|
128
|
+
expect(buckets2.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group2"]').bucket]);
|
|
117
129
|
});
|
|
118
130
|
test('it should use the latest version after updates', async () => {
|
|
119
131
|
await using factory = await generateStorageFactory();
|
|
@@ -127,6 +139,7 @@ bucket_definitions:
|
|
|
127
139
|
data: []
|
|
128
140
|
`, { storageVersion }));
|
|
129
141
|
const bucketStorage = factory.getInstance(syncRules);
|
|
142
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
130
143
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
131
144
|
const table = await test_utils.resolveTestTable(writer, 'todos', ['id', 'list_id'], config);
|
|
132
145
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -165,17 +178,23 @@ bucket_definitions:
|
|
|
165
178
|
// There removal operation for the association of `list2`::`todo2` should not interfere with the new
|
|
166
179
|
// association of `list1`::`todo2`
|
|
167
180
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
168
|
-
const parameters =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: { list_id: ['list1', 'list2'] } }), {});
|
|
182
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
183
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
184
|
+
async getParameterSets(lookups) {
|
|
185
|
+
expect(lookups.map((l) => JSON.stringify(l.indexKey)).sort()).toEqual(['["list1"]', '["list2"]']);
|
|
186
|
+
const results = await checkpoint.getParameterSets(lookups, 1000);
|
|
187
|
+
const allRows = results.flatMap(({ rows }) => rows);
|
|
188
|
+
expect(allRows.sort((a, b) => a.todo_id.localeCompare(b.todo_id))).toEqual([
|
|
189
|
+
{ todo_id: 'todo1' },
|
|
190
|
+
{ todo_id: 'todo2' }
|
|
191
|
+
]);
|
|
192
|
+
return results;
|
|
178
193
|
}
|
|
194
|
+
});
|
|
195
|
+
expect(buckets.map((b) => b.bucket).sort()).toEqual([
|
|
196
|
+
bucketRequest(syncRules, 'mybucket["todo1"]').bucket,
|
|
197
|
+
bucketRequest(syncRules, 'mybucket["todo2"]').bucket
|
|
179
198
|
]);
|
|
180
199
|
});
|
|
181
200
|
test('save and load parameters with different number types', async () => {
|
|
@@ -190,6 +209,7 @@ bucket_definitions:
|
|
|
190
209
|
storageVersion
|
|
191
210
|
}));
|
|
192
211
|
const bucketStorage = factory.getInstance(syncRules);
|
|
212
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
193
213
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
194
214
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
195
215
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -206,18 +226,30 @@ bucket_definitions:
|
|
|
206
226
|
afterReplicaId: test_utils.rid('t1')
|
|
207
227
|
});
|
|
208
228
|
await writer.commit('1/1');
|
|
209
|
-
const TEST_PARAMS = { group_id: 'group1' };
|
|
210
229
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
211
|
-
const
|
|
212
|
-
|
|
230
|
+
const testQuery = async (jwtParameters, expectedParameterSets) => {
|
|
231
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: jwtParameters }), {});
|
|
232
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
233
|
+
return await querier.queryDynamicBucketDescriptions({
|
|
234
|
+
async getParameterSets(lookups) {
|
|
235
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
236
|
+
if (expectedParameterSets.length == 0) {
|
|
237
|
+
expect(parameter_sets).toEqual([]);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: expectedParameterSets }]);
|
|
241
|
+
}
|
|
242
|
+
return parameter_sets;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
expect(await testQuery({ n1: 314n, f2: 314, f3: 3.14 }, [{ group_id: 'group1' }])).toMatchObject([
|
|
247
|
+
{ bucket: bucketRequest(syncRules, 'mybucket["group1"]').bucket }
|
|
213
248
|
]);
|
|
214
|
-
expect(
|
|
215
|
-
|
|
216
|
-
ScopedParameterLookup.direct(MYBUCKET_1, [314, 314n, 3.14])
|
|
249
|
+
expect(await testQuery({ n1: 314, f2: 314n, f3: 3.14 }, [{ group_id: 'group1' }])).toMatchObject([
|
|
250
|
+
{ bucket: bucketRequest(syncRules, 'mybucket["group1"]').bucket }
|
|
217
251
|
]);
|
|
218
|
-
expect(
|
|
219
|
-
const parameters3 = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, [314n, 314, 3])]);
|
|
220
|
-
expect(parameters3).toEqual([]);
|
|
252
|
+
expect(await testQuery({ n1: 314n, f2: 314, f3: 3 }, [])).toEqual([]);
|
|
221
253
|
});
|
|
222
254
|
test('save and load parameters with large numbers', async () => {
|
|
223
255
|
// This ensures serialization / deserialization of "current_data" is done correctly.
|
|
@@ -234,6 +266,7 @@ bucket_definitions:
|
|
|
234
266
|
storageVersion
|
|
235
267
|
}));
|
|
236
268
|
const bucketStorage = factory.getInstance(syncRules);
|
|
269
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
237
270
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
238
271
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
239
272
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -260,12 +293,19 @@ bucket_definitions:
|
|
|
260
293
|
afterReplicaId: test_utils.rid('t1')
|
|
261
294
|
});
|
|
262
295
|
await writer.commit('1/1');
|
|
263
|
-
const TEST_PARAMS = { group_id: 'group1' };
|
|
264
296
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
297
|
+
const n1 = 1152921504606846976n;
|
|
298
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: { n1 } }), {});
|
|
299
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
300
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
301
|
+
getParameterSets: async (lookups) => {
|
|
302
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([[n1]]);
|
|
303
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
304
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1' }] }]);
|
|
305
|
+
return parameter_sets;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1"]').bucket]);
|
|
269
309
|
});
|
|
270
310
|
test('save and load parameters with workspaceId', async () => {
|
|
271
311
|
await using factory = await generateStorageFactory();
|
|
@@ -279,7 +319,7 @@ bucket_definitions:
|
|
|
279
319
|
`, {
|
|
280
320
|
storageVersion
|
|
281
321
|
}));
|
|
282
|
-
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).
|
|
322
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
283
323
|
const bucketStorage = factory.getInstance(syncRules);
|
|
284
324
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
285
325
|
const workspaceTable = await test_utils.resolveTestTable(writer, 'workspace', ['id'], config);
|
|
@@ -299,9 +339,9 @@ bucket_definitions:
|
|
|
299
339
|
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
300
340
|
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
301
341
|
async getParameterSets(lookups) {
|
|
302
|
-
expect(lookups
|
|
303
|
-
const parameter_sets = await checkpoint.getParameterSets(lookups);
|
|
304
|
-
expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }]);
|
|
342
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['u1']]);
|
|
343
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
344
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ workspace_id: 'workspace1' }] }]);
|
|
305
345
|
return parameter_sets;
|
|
306
346
|
}
|
|
307
347
|
});
|
|
@@ -326,7 +366,7 @@ bucket_definitions:
|
|
|
326
366
|
`, {
|
|
327
367
|
storageVersion
|
|
328
368
|
}));
|
|
329
|
-
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).
|
|
369
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
330
370
|
const bucketStorage = factory.getInstance(syncRules);
|
|
331
371
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
332
372
|
const workspaceTable = await test_utils.resolveTestTable(writer, 'workspace', undefined, config);
|
|
@@ -364,10 +404,13 @@ bucket_definitions:
|
|
|
364
404
|
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
365
405
|
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
366
406
|
async getParameterSets(lookups) {
|
|
367
|
-
expect(lookups
|
|
368
|
-
const parameter_sets = await checkpoint.getParameterSets(lookups);
|
|
369
|
-
parameter_sets
|
|
370
|
-
|
|
407
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([[]]);
|
|
408
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
409
|
+
expect(parameter_sets).toHaveLength(1);
|
|
410
|
+
const [{ lookup, rows }] = parameter_sets;
|
|
411
|
+
expect(lookup).toEqual(lookups[0]);
|
|
412
|
+
rows.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
413
|
+
expect(rows).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
|
|
371
414
|
return parameter_sets;
|
|
372
415
|
}
|
|
373
416
|
});
|
|
@@ -401,7 +444,7 @@ bucket_definitions:
|
|
|
401
444
|
`, {
|
|
402
445
|
storageVersion
|
|
403
446
|
}));
|
|
404
|
-
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).
|
|
447
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
405
448
|
const bucketStorage = factory.getInstance(syncRules);
|
|
406
449
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
407
450
|
const workspaceTable = await test_utils.resolveTestTable(writer, 'workspace', undefined, config);
|
|
@@ -455,15 +498,15 @@ bucket_definitions:
|
|
|
455
498
|
const buckets = (await querier.queryDynamicBucketDescriptions({
|
|
456
499
|
async getParameterSets(lookups) {
|
|
457
500
|
foundLookups.push(...lookups);
|
|
458
|
-
const output = await checkpoint.getParameterSets(lookups);
|
|
459
|
-
|
|
501
|
+
const output = await checkpoint.getParameterSets(lookups, 1000);
|
|
502
|
+
for (const { rows } of output) {
|
|
503
|
+
parameter_sets.push(...rows);
|
|
504
|
+
}
|
|
460
505
|
return output;
|
|
461
506
|
}
|
|
462
507
|
})).map((e) => e.bucket);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '2'), ['u1'])
|
|
466
|
-
]);
|
|
508
|
+
// Not testing the scope anymore - the exact format depends on storage version
|
|
509
|
+
expect(foundLookups.map((l) => l.indexKey)).toEqual([[], ['u1']]);
|
|
467
510
|
parameter_sets.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
468
511
|
expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
|
|
469
512
|
buckets.sort();
|
|
@@ -484,6 +527,7 @@ bucket_definitions:
|
|
|
484
527
|
storageVersion
|
|
485
528
|
}));
|
|
486
529
|
const bucketStorage = factory.getInstance(syncRules);
|
|
530
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
487
531
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
488
532
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
489
533
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -501,8 +545,17 @@ bucket_definitions:
|
|
|
501
545
|
await writer.truncate([testTable]);
|
|
502
546
|
await writer.flush();
|
|
503
547
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
504
|
-
const parameters =
|
|
505
|
-
|
|
548
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
|
|
549
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
550
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
551
|
+
async getParameterSets(lookups) {
|
|
552
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
|
|
553
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
554
|
+
expect(parameter_sets).toEqual([]);
|
|
555
|
+
return parameter_sets;
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
expect(buckets).toEqual([]);
|
|
506
559
|
});
|
|
507
560
|
test('invalidate cached parsed sync rules', async () => {
|
|
508
561
|
await using bucketStorageFactory = await generateStorageFactory();
|
|
@@ -546,6 +599,7 @@ streams:
|
|
|
546
599
|
WHERE data.foo = param.bar AND param.baz = auth.user_id()
|
|
547
600
|
`));
|
|
548
601
|
const bucketStorage = factory.getInstance(syncRules);
|
|
602
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
549
603
|
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
550
604
|
const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
|
|
551
605
|
await writer.markAllSnapshotDone('1/1');
|
|
@@ -560,13 +614,330 @@ streams:
|
|
|
560
614
|
});
|
|
561
615
|
await writer.commit('1/1');
|
|
562
616
|
const checkpoint = await bucketStorage.getCheckpoint();
|
|
563
|
-
const parameters =
|
|
564
|
-
|
|
617
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'baz' }), {});
|
|
618
|
+
const querier = sync_rules.getBucketParameterQuerier({
|
|
619
|
+
...test_utils.querierOptions(parameters),
|
|
620
|
+
streams: {
|
|
621
|
+
stream: [
|
|
622
|
+
{
|
|
623
|
+
priorityOverride: null,
|
|
624
|
+
parameters: null,
|
|
625
|
+
opaque_id: 123
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
}
|
|
629
|
+
}).querier;
|
|
630
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
631
|
+
async getParameterSets(lookups) {
|
|
632
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['baz']]);
|
|
633
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
634
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'bar' }] }]);
|
|
635
|
+
return parameter_sets;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
expect(buckets).toHaveLength(1);
|
|
639
|
+
expect(buckets).toMatchObject([
|
|
640
|
+
{
|
|
641
|
+
bucket: expect.stringMatching(/stream.*\["bar"\]$/),
|
|
642
|
+
definition: 'stream',
|
|
643
|
+
inclusion_reasons: [{ subscription: 123 }],
|
|
644
|
+
priority: 3
|
|
645
|
+
}
|
|
565
646
|
]);
|
|
566
|
-
|
|
647
|
+
});
|
|
648
|
+
test('respects parameter limit', async () => {
|
|
649
|
+
await using factory = await generateStorageFactory();
|
|
650
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
|
|
651
|
+
config:
|
|
652
|
+
edition: 3
|
|
653
|
+
|
|
654
|
+
streams:
|
|
655
|
+
a:
|
|
656
|
+
auto_subscribe: true
|
|
657
|
+
query: SELECT * FROM a WHERE id IN (SELECT id FROM b)
|
|
658
|
+
`, {
|
|
659
|
+
storageVersion
|
|
660
|
+
}));
|
|
661
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
662
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
663
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
664
|
+
const testTable = await test_utils.resolveTestTable(writer, 'b', ['id'], config);
|
|
665
|
+
await writer.markAllSnapshotDone('1/1');
|
|
666
|
+
for (let i = 0; i < 10; i++) {
|
|
667
|
+
await writer.save({
|
|
668
|
+
sourceTable: testTable,
|
|
669
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
670
|
+
after: {
|
|
671
|
+
id: `t${i}`
|
|
672
|
+
},
|
|
673
|
+
afterReplicaId: test_utils.rid(`t${i}`)
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
await writer.commit('1/1');
|
|
677
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
678
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'u' }), {});
|
|
679
|
+
const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
|
|
680
|
+
await expect(querier.queryDynamicBucketDescriptions({
|
|
681
|
+
async getParameterSets(lookups) {
|
|
682
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 5);
|
|
683
|
+
return parameter_sets;
|
|
684
|
+
}
|
|
685
|
+
})).rejects.toThrow('Too many parameter results (limit was 5)');
|
|
686
|
+
});
|
|
687
|
+
test('sync streams store multiple parameter outputs for a single source row and lookup', async () => {
|
|
688
|
+
await using factory = await generateStorageFactory();
|
|
689
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
|
|
690
|
+
config:
|
|
691
|
+
edition: 3
|
|
692
|
+
streams:
|
|
693
|
+
chat:
|
|
694
|
+
auto_subscribe: true
|
|
695
|
+
query: |
|
|
696
|
+
SELECT a.*
|
|
697
|
+
FROM a, b, json_each(b.x) x, json_each(b.y) y
|
|
698
|
+
WHERE a.x = x.value AND y.value = auth.user_id()
|
|
699
|
+
`));
|
|
700
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
701
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
702
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
703
|
+
const tableB = await test_utils.resolveTestTable(writer, 'b', ['id'], config);
|
|
704
|
+
const firstState = {
|
|
705
|
+
id: 'id0',
|
|
706
|
+
x: JSON.stringify(['x1', 'x2']),
|
|
707
|
+
y: JSON.stringify(['y1', 'y2'])
|
|
708
|
+
};
|
|
709
|
+
const secondState = {
|
|
710
|
+
id: 'id0',
|
|
711
|
+
x: JSON.stringify(['x2']),
|
|
712
|
+
y: JSON.stringify(['y1', 'y2'])
|
|
713
|
+
};
|
|
714
|
+
const thirdState = {
|
|
715
|
+
id: 'id0',
|
|
716
|
+
x: JSON.stringify(['x2']),
|
|
717
|
+
y: JSON.stringify(['y2'])
|
|
718
|
+
};
|
|
719
|
+
const replicaId = test_utils.rid('id0');
|
|
720
|
+
await writer.markAllSnapshotDone('1/1');
|
|
721
|
+
await writer.save({
|
|
722
|
+
sourceTable: tableB,
|
|
723
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
724
|
+
after: firstState,
|
|
725
|
+
afterReplicaId: replicaId
|
|
726
|
+
});
|
|
727
|
+
await writer.commit('1/1');
|
|
728
|
+
let checkpoint = await bucketStorage.getCheckpoint();
|
|
729
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'y1' }), {});
|
|
730
|
+
const querier = sync_rules.getBucketParameterQuerier({
|
|
731
|
+
...test_utils.querierOptions(parameters)
|
|
732
|
+
}).querier;
|
|
733
|
+
let buckets = await querier.queryDynamicBucketDescriptions({
|
|
734
|
+
async getParameterSets(lookups) {
|
|
735
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
|
|
736
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
737
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'x1' }, { '0': 'x2' }] }]);
|
|
738
|
+
return parameter_sets;
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
expect(buckets).toMatchObject([
|
|
742
|
+
{
|
|
743
|
+
bucket: expect.stringMatching(/chat.*\["x1"\]$/),
|
|
744
|
+
definition: 'chat',
|
|
745
|
+
inclusion_reasons: ['default'],
|
|
746
|
+
priority: 3
|
|
747
|
+
},
|
|
567
748
|
{
|
|
568
|
-
|
|
749
|
+
bucket: expect.stringMatching(/chat.*\["x2"\]$/),
|
|
750
|
+
definition: 'chat',
|
|
751
|
+
inclusion_reasons: ['default'],
|
|
752
|
+
priority: 3
|
|
753
|
+
}
|
|
754
|
+
]);
|
|
755
|
+
// Make the x2 bucket inaccessible
|
|
756
|
+
await writer.save({
|
|
757
|
+
sourceTable: tableB,
|
|
758
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
759
|
+
before: firstState,
|
|
760
|
+
after: secondState,
|
|
761
|
+
beforeReplicaId: replicaId,
|
|
762
|
+
afterReplicaId: replicaId
|
|
763
|
+
});
|
|
764
|
+
await writer.commit('1/2');
|
|
765
|
+
checkpoint = await bucketStorage.getCheckpoint();
|
|
766
|
+
buckets = await querier.queryDynamicBucketDescriptions({
|
|
767
|
+
async getParameterSets(lookups) {
|
|
768
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
|
|
769
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
770
|
+
expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'x2' }] }]);
|
|
771
|
+
return parameter_sets;
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
expect(buckets.map((bkt) => bkt.bucket)).toStrictEqual([expect.stringContaining('x2')]);
|
|
775
|
+
// Second update, remove user from inputs
|
|
776
|
+
await writer.save({
|
|
777
|
+
sourceTable: tableB,
|
|
778
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
779
|
+
before: secondState,
|
|
780
|
+
after: thirdState,
|
|
781
|
+
beforeReplicaId: replicaId,
|
|
782
|
+
afterReplicaId: replicaId
|
|
783
|
+
});
|
|
784
|
+
await writer.commit('1/3');
|
|
785
|
+
checkpoint = await bucketStorage.getCheckpoint();
|
|
786
|
+
buckets = await querier.queryDynamicBucketDescriptions({
|
|
787
|
+
async getParameterSets(lookups) {
|
|
788
|
+
expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
|
|
789
|
+
const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
|
|
790
|
+
expect(parameter_sets).toHaveLength(0);
|
|
791
|
+
return parameter_sets;
|
|
569
792
|
}
|
|
793
|
+
});
|
|
794
|
+
expect(buckets).toHaveLength(0);
|
|
795
|
+
});
|
|
796
|
+
test('can request multiple lookups at once', async () => {
|
|
797
|
+
await using factory = await generateStorageFactory();
|
|
798
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
|
|
799
|
+
config:
|
|
800
|
+
edition: 3
|
|
801
|
+
streams:
|
|
802
|
+
a:
|
|
803
|
+
auto_subscribe: true
|
|
804
|
+
query: SELECT * FROM a WHERE p IN (SELECT id FROM param_a WHERE u = auth.user_id())
|
|
805
|
+
b:
|
|
806
|
+
auto_subscribe: true
|
|
807
|
+
query: SELECT * FROM b WHERE p IN (SELECT id FROM param_b WHERE u = auth.user_id())
|
|
808
|
+
`));
|
|
809
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
810
|
+
const parsedSyncRules = syncRules.parsed(test_utils.PARSE_OPTIONS);
|
|
811
|
+
const hydrationState = parsedSyncRules.hydrationState;
|
|
812
|
+
const syncConfig = parsedSyncRules.syncConfigWithErrors.config;
|
|
813
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
814
|
+
const paramATable = await test_utils.resolveTestTable(writer, 'param_a', ['id'], config, 1);
|
|
815
|
+
const paramBTable = await test_utils.resolveTestTable(writer, 'param_b', ['id'], config, 2);
|
|
816
|
+
const replicaId = test_utils.rid('id');
|
|
817
|
+
// Insert the same row into param_a and param_b
|
|
818
|
+
await writer.markAllSnapshotDone('1/1');
|
|
819
|
+
await writer.save({
|
|
820
|
+
sourceTable: paramATable,
|
|
821
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
822
|
+
after: {
|
|
823
|
+
id: 'id',
|
|
824
|
+
u: 'user'
|
|
825
|
+
},
|
|
826
|
+
afterReplicaId: replicaId
|
|
827
|
+
});
|
|
828
|
+
await writer.save({
|
|
829
|
+
sourceTable: paramBTable,
|
|
830
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
831
|
+
after: {
|
|
832
|
+
id: 'id',
|
|
833
|
+
u: 'user'
|
|
834
|
+
},
|
|
835
|
+
afterReplicaId: replicaId
|
|
836
|
+
});
|
|
837
|
+
await writer.commit('1/1');
|
|
838
|
+
function findParameterOnTable(name) {
|
|
839
|
+
for (const source of syncConfig.bucketParameterLookupSources) {
|
|
840
|
+
for (const param of source.getSourceTables()) {
|
|
841
|
+
if (param.name == name)
|
|
842
|
+
return source;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
throw new Error(`Expected parameter index on ${name}`);
|
|
846
|
+
}
|
|
847
|
+
// Run two lookups on separate parameter indexes.
|
|
848
|
+
const lookupA = ScopedParameterLookup.normalized(hydrationState.getParameterIndexLookupScope(findParameterOnTable('param_a')), UnscopedParameterLookup.normalized(['user']));
|
|
849
|
+
const lookupB = ScopedParameterLookup.normalized(hydrationState.getParameterIndexLookupScope(findParameterOnTable('param_b')), UnscopedParameterLookup.normalized(['user']));
|
|
850
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
851
|
+
const parameterSets = await checkpoint.getParameterSets([lookupA, lookupB], 1000);
|
|
852
|
+
const expectedRow = { '0': 'id' };
|
|
853
|
+
let foundLookupA = false, foundLookupB = false;
|
|
854
|
+
// We should get the same row on both, with information on which lookup contributed which row.
|
|
855
|
+
for (const { lookup, rows } of parameterSets) {
|
|
856
|
+
if (lookup === lookupA) {
|
|
857
|
+
foundLookupA = true;
|
|
858
|
+
expect(rows).toStrictEqual([expectedRow]);
|
|
859
|
+
}
|
|
860
|
+
else if (lookup === lookupB) {
|
|
861
|
+
foundLookupB = true;
|
|
862
|
+
expect(rows).toStrictEqual([expectedRow]);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
throw new Error('unexpected lookup in results');
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
expect(foundLookupA).toBeTruthy();
|
|
869
|
+
expect(foundLookupB).toBeTruthy();
|
|
870
|
+
});
|
|
871
|
+
test('sync streams preserve duplicate downstream lookups with different provenance', async () => {
|
|
872
|
+
await using factory = await generateStorageFactory();
|
|
873
|
+
const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
|
|
874
|
+
config:
|
|
875
|
+
edition: 3
|
|
876
|
+
streams:
|
|
877
|
+
stream:
|
|
878
|
+
auto_subscribe: true
|
|
879
|
+
query: |
|
|
880
|
+
SELECT a.*
|
|
881
|
+
FROM a, b, c
|
|
882
|
+
WHERE a.x = b.x
|
|
883
|
+
AND a.z = c.z
|
|
884
|
+
AND b.y = c.y
|
|
885
|
+
AND c.u = auth.user_id()
|
|
886
|
+
`, {
|
|
887
|
+
storageVersion
|
|
888
|
+
}));
|
|
889
|
+
const bucketStorage = factory.getInstance(syncRules);
|
|
890
|
+
const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
|
|
891
|
+
await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
|
|
892
|
+
const tableB = await test_utils.resolveTestTable(writer, 'b', ['id'], config, 1);
|
|
893
|
+
const tableC = await test_utils.resolveTestTable(writer, 'c', ['id'], config, 2);
|
|
894
|
+
await writer.markAllSnapshotDone('1/1');
|
|
895
|
+
await writer.save({
|
|
896
|
+
sourceTable: tableB,
|
|
897
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
898
|
+
after: {
|
|
899
|
+
id: 'b1',
|
|
900
|
+
y: 'shared-y',
|
|
901
|
+
x: 'x-from-shared-y'
|
|
902
|
+
},
|
|
903
|
+
afterReplicaId: test_utils.rid('b1')
|
|
904
|
+
});
|
|
905
|
+
await writer.save({
|
|
906
|
+
sourceTable: tableC,
|
|
907
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
908
|
+
after: {
|
|
909
|
+
id: 'c1',
|
|
910
|
+
u: 'user1',
|
|
911
|
+
y: 'shared-y',
|
|
912
|
+
z: 'z1'
|
|
913
|
+
},
|
|
914
|
+
afterReplicaId: test_utils.rid('c1')
|
|
915
|
+
});
|
|
916
|
+
await writer.save({
|
|
917
|
+
sourceTable: tableC,
|
|
918
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
919
|
+
after: {
|
|
920
|
+
id: 'c2',
|
|
921
|
+
u: 'user1',
|
|
922
|
+
y: 'shared-y',
|
|
923
|
+
z: 'z2'
|
|
924
|
+
},
|
|
925
|
+
afterReplicaId: test_utils.rid('c2')
|
|
926
|
+
});
|
|
927
|
+
await writer.commit('1/1');
|
|
928
|
+
const checkpoint = await bucketStorage.getCheckpoint();
|
|
929
|
+
const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
|
|
930
|
+
const querier = sync_rules.getBucketParameterQuerier({
|
|
931
|
+
...test_utils.querierOptions(parameters)
|
|
932
|
+
}).querier;
|
|
933
|
+
const buckets = await querier.queryDynamicBucketDescriptions({
|
|
934
|
+
async getParameterSets(lookups) {
|
|
935
|
+
return checkpoint.getParameterSets(lookups, 1000);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
expect(buckets.map((bucket) => bucket.bucket).sort()).toStrictEqual([
|
|
939
|
+
expect.stringMatching(/stream.*\["x-from-shared-y","z1"\]$/),
|
|
940
|
+
expect.stringMatching(/stream.*\["x-from-shared-y","z2"\]$/)
|
|
570
941
|
]);
|
|
571
942
|
});
|
|
572
943
|
}
|