@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/test-utils/AbstractStreamTestContext.d.ts +51 -0
  3. package/dist/test-utils/AbstractStreamTestContext.js +143 -0
  4. package/dist/test-utils/AbstractStreamTestContext.js.map +1 -0
  5. package/dist/test-utils/StorageDataHelpers.d.ts +8 -0
  6. package/dist/test-utils/StorageDataHelpers.js +33 -0
  7. package/dist/test-utils/StorageDataHelpers.js.map +1 -0
  8. package/dist/test-utils/general-utils.d.ts +3 -8
  9. package/dist/test-utils/general-utils.js +28 -26
  10. package/dist/test-utils/general-utils.js.map +1 -1
  11. package/dist/test-utils/test-utils-index.d.ts +2 -0
  12. package/dist/test-utils/test-utils-index.js +2 -0
  13. package/dist/test-utils/test-utils-index.js.map +1 -1
  14. package/dist/tests/register-data-storage-data-tests.js +3 -3
  15. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  16. package/dist/tests/register-data-storage-parameter-tests.js +436 -65
  17. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  18. package/dist/tests/register-parameter-compacting-tests.js +8 -8
  19. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  20. package/dist/tests/util.d.ts +1 -2
  21. package/dist/tests/util.js +8 -5
  22. package/dist/tests/util.js.map +1 -1
  23. package/package.json +7 -6
  24. package/src/test-utils/AbstractStreamTestContext.ts +179 -0
  25. package/src/test-utils/StorageDataHelpers.ts +44 -0
  26. package/src/test-utils/general-utils.ts +31 -31
  27. package/src/test-utils/test-utils-index.ts +2 -0
  28. package/src/tests/register-data-storage-data-tests.ts +3 -3
  29. package/src/tests/register-data-storage-parameter-tests.ts +519 -67
  30. package/src/tests/register-parameter-compacting-tests.ts +8 -8
  31. package/src/tests/util.ts +12 -9
  32. package/tsconfig.json +3 -0
  33. 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 = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
61
- expect(parameters).toEqual([
62
- {
63
- group_id: 'group1a'
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 = await checkpoint2.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
105
- expect(parameters).toEqual([
106
- {
107
- group_id: 'group2'
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
- // Use the checkpoint to get older data if relevant
111
- const parameters2 = await checkpoint1.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
112
- expect(parameters2).toEqual([
113
- {
114
- group_id: 'group1'
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 = await checkpoint.getParameterSets([
169
- ScopedParameterLookup.direct(MYBUCKET_1, ['list1']),
170
- ScopedParameterLookup.direct(MYBUCKET_1, ['list2'])
171
- ]);
172
- expect(parameters.sort((a, b) => a.todo_id.localeCompare(b.todo_id))).toEqual([
173
- {
174
- todo_id: 'todo1'
175
- },
176
- {
177
- todo_id: 'todo2'
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 parameters1 = await checkpoint.getParameterSets([
212
- ScopedParameterLookup.direct(MYBUCKET_1, [314n, 314, 3.14])
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(parameters1).toEqual([TEST_PARAMS]);
215
- const parameters2 = await checkpoint.getParameterSets([
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(parameters2).toEqual([TEST_PARAMS]);
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 parameters1 = await checkpoint.getParameterSets([
266
- ScopedParameterLookup.direct(MYBUCKET_1, [1152921504606846976n])
267
- ]);
268
- expect(parameters1).toEqual([TEST_PARAMS]);
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).hydratedSyncRules();
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).toEqual([ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '1'), ['u1'])]);
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).hydratedSyncRules();
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).toEqual([ScopedParameterLookup.direct(parameterLookupScope('by_public_workspace', '1'), [])]);
368
- const parameter_sets = await checkpoint.getParameterSets(lookups);
369
- parameter_sets.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
370
- expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
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).hydratedSyncRules();
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
- parameter_sets.push(...output);
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
- expect(foundLookups).toEqual([
464
- ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '1'), []),
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 = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
505
- expect(parameters).toEqual([]);
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 = await checkpoint.getParameterSets([
564
- ScopedParameterLookup.direct(parameterLookupScope('lookup', '0'), ['baz'])
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
- expect(parameters).toEqual([
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
- '0': 'bar'
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
  }