@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,14 @@
1
1
  import { CURRENT_STORAGE_VERSION, JwtPayload, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
2
- import { RequestParameters, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
2
+ import {
3
+ ParameterIndexLookupCreator,
4
+ RequestParameters,
5
+ ScopedParameterLookup,
6
+ SqliteJsonRow,
7
+ UnscopedParameterLookup
8
+ } from '@powersync/service-sync-rules';
3
9
  import { expect, test } from 'vitest';
4
10
  import * as test_utils from '../test-utils/test-utils-index.js';
5
11
  import { bucketRequest } from '../test-utils/test-utils-index.js';
6
- import { parameterLookupScope } from './util.js';
7
12
 
8
13
  /**
9
14
  * @example
@@ -18,7 +23,6 @@ import { parameterLookupScope } from './util.js';
18
23
  export function registerDataStorageParameterTests(config: storage.TestStorageConfig) {
19
24
  const generateStorageFactory = config.factory;
20
25
  const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
21
- const MYBUCKET_1 = parameterLookupScope('mybucket', '1');
22
26
 
23
27
  test('save and load parameters', async () => {
24
28
  await using factory = await generateStorageFactory();
@@ -37,6 +41,7 @@ bucket_definitions:
37
41
  )
38
42
  );
39
43
  const bucketStorage = factory.getInstance(syncRules);
44
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
40
45
 
41
46
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
42
47
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -69,12 +74,20 @@ bucket_definitions:
69
74
  await writer.commit('1/1');
70
75
 
71
76
  const checkpoint = await bucketStorage.getCheckpoint();
72
- const parameters = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
73
- expect(parameters).toEqual([
74
- {
75
- group_id: 'group1a'
77
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
78
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
79
+
80
+ const buckets = await querier.queryDynamicBucketDescriptions({
81
+ async getParameterSets(lookups) {
82
+ expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
83
+
84
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
85
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1a' }] }]);
86
+ return parameter_sets;
76
87
  }
77
- ]);
88
+ });
89
+
90
+ expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1a"]').bucket]);
78
91
  });
79
92
 
80
93
  test('it should use the latest version', async () => {
@@ -94,6 +107,7 @@ bucket_definitions:
94
107
  )
95
108
  );
96
109
  const bucketStorage = factory.getInstance(syncRules);
110
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
97
111
 
98
112
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
99
113
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -121,20 +135,30 @@ bucket_definitions:
121
135
  await writer.commit('1/2');
122
136
  const checkpoint2 = await bucketStorage.getCheckpoint();
123
137
 
124
- const parameters = await checkpoint2.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
125
- expect(parameters).toEqual([
126
- {
127
- group_id: 'group2'
138
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
139
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
140
+
141
+ const buckets1 = await querier.queryDynamicBucketDescriptions({
142
+ async getParameterSets(lookups) {
143
+ expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
144
+
145
+ const parameter_sets = await checkpoint1.getParameterSets(lookups, 1000);
146
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1' }] }]);
147
+ return parameter_sets;
128
148
  }
129
- ]);
149
+ });
150
+ expect(buckets1.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1"]').bucket]);
130
151
 
131
- // Use the checkpoint to get older data if relevant
132
- const parameters2 = await checkpoint1.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
133
- expect(parameters2).toEqual([
134
- {
135
- group_id: 'group1'
152
+ const buckets2 = await querier.queryDynamicBucketDescriptions({
153
+ async getParameterSets(lookups) {
154
+ expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
155
+
156
+ const parameter_sets = await checkpoint2.getParameterSets(lookups, 1000);
157
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group2' }] }]);
158
+ return parameter_sets;
136
159
  }
137
- ]);
160
+ });
161
+ expect(buckets2.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group2"]').bucket]);
138
162
  });
139
163
 
140
164
  test('it should use the latest version after updates', async () => {
@@ -154,6 +178,7 @@ bucket_definitions:
154
178
  )
155
179
  );
156
180
  const bucketStorage = factory.getInstance(syncRules);
181
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
157
182
 
158
183
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
159
184
  const table = await test_utils.resolveTestTable(writer, 'todos', ['id', 'list_id'], config);
@@ -197,18 +222,29 @@ bucket_definitions:
197
222
  // There removal operation for the association of `list2`::`todo2` should not interfere with the new
198
223
  // association of `list1`::`todo2`
199
224
  const checkpoint = await bucketStorage.getCheckpoint();
200
- const parameters = await checkpoint.getParameterSets([
201
- ScopedParameterLookup.direct(MYBUCKET_1, ['list1']),
202
- ScopedParameterLookup.direct(MYBUCKET_1, ['list2'])
203
- ]);
225
+ const parameters = new RequestParameters(
226
+ new JwtPayload({ sub: 'u1', parameters: { list_id: ['list1', 'list2'] } }),
227
+ {}
228
+ );
229
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
204
230
 
205
- expect(parameters.sort((a, b) => (a.todo_id as string).localeCompare(b.todo_id as string))).toEqual([
206
- {
207
- todo_id: 'todo1'
208
- },
209
- {
210
- todo_id: 'todo2'
231
+ const buckets = await querier.queryDynamicBucketDescriptions({
232
+ async getParameterSets(lookups) {
233
+ expect(lookups.map((l) => JSON.stringify(l.indexKey)).sort()).toEqual(['["list1"]', '["list2"]']);
234
+
235
+ const results = await checkpoint.getParameterSets(lookups, 1000);
236
+ const allRows = results.flatMap(({ rows }) => rows);
237
+ expect(allRows.sort((a, b) => (a.todo_id as string).localeCompare(b.todo_id as string))).toEqual([
238
+ { todo_id: 'todo1' },
239
+ { todo_id: 'todo2' }
240
+ ]);
241
+ return results;
211
242
  }
243
+ });
244
+
245
+ expect(buckets.map((b) => b.bucket).sort()).toEqual([
246
+ bucketRequest(syncRules, 'mybucket["todo1"]').bucket,
247
+ bucketRequest(syncRules, 'mybucket["todo2"]').bucket
212
248
  ]);
213
249
  });
214
250
 
@@ -229,6 +265,7 @@ bucket_definitions:
229
265
  )
230
266
  );
231
267
  const bucketStorage = factory.getInstance(syncRules);
268
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
232
269
 
233
270
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
234
271
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -248,20 +285,31 @@ bucket_definitions:
248
285
 
249
286
  await writer.commit('1/1');
250
287
 
251
- const TEST_PARAMS = { group_id: 'group1' };
252
-
253
288
  const checkpoint = await bucketStorage.getCheckpoint();
289
+ const testQuery = async (jwtParameters: Record<string, any>, expectedParameterSets: SqliteJsonRow[]) => {
290
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: jwtParameters }), {});
291
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
254
292
 
255
- const parameters1 = await checkpoint.getParameterSets([
256
- ScopedParameterLookup.direct(MYBUCKET_1, [314n, 314, 3.14])
293
+ return await querier.queryDynamicBucketDescriptions({
294
+ async getParameterSets(lookups) {
295
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
296
+ if (expectedParameterSets.length == 0) {
297
+ expect(parameter_sets).toEqual([]);
298
+ } else {
299
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: expectedParameterSets }]);
300
+ }
301
+ return parameter_sets;
302
+ }
303
+ });
304
+ };
305
+
306
+ expect(await testQuery({ n1: 314n, f2: 314, f3: 3.14 }, [{ group_id: 'group1' }])).toMatchObject([
307
+ { bucket: bucketRequest(syncRules, 'mybucket["group1"]').bucket }
257
308
  ]);
258
- expect(parameters1).toEqual([TEST_PARAMS]);
259
- const parameters2 = await checkpoint.getParameterSets([
260
- ScopedParameterLookup.direct(MYBUCKET_1, [314, 314n, 3.14])
309
+ expect(await testQuery({ n1: 314, f2: 314n, f3: 3.14 }, [{ group_id: 'group1' }])).toMatchObject([
310
+ { bucket: bucketRequest(syncRules, 'mybucket["group1"]').bucket }
261
311
  ]);
262
- expect(parameters2).toEqual([TEST_PARAMS]);
263
- const parameters3 = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, [314n, 314, 3])]);
264
- expect(parameters3).toEqual([]);
312
+ expect(await testQuery({ n1: 314n, f2: 314, f3: 3 }, [])).toEqual([]);
265
313
  });
266
314
 
267
315
  test('save and load parameters with large numbers', async () => {
@@ -285,6 +333,7 @@ bucket_definitions:
285
333
  )
286
334
  );
287
335
  const bucketStorage = factory.getInstance(syncRules);
336
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
288
337
 
289
338
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
290
339
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -315,14 +364,23 @@ bucket_definitions:
315
364
 
316
365
  await writer.commit('1/1');
317
366
 
318
- const TEST_PARAMS = { group_id: 'group1' };
319
-
320
367
  const checkpoint = await bucketStorage.getCheckpoint();
321
368
 
322
- const parameters1 = await checkpoint.getParameterSets([
323
- ScopedParameterLookup.direct(MYBUCKET_1, [1152921504606846976n])
324
- ]);
325
- expect(parameters1).toEqual([TEST_PARAMS]);
369
+ const n1 = 1152921504606846976n;
370
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'u1', parameters: { n1 } }), {});
371
+
372
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
373
+ const buckets = await querier.queryDynamicBucketDescriptions({
374
+ getParameterSets: async (lookups) => {
375
+ expect(lookups.map((l) => l.indexKey)).toEqual([[n1]]);
376
+
377
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
378
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ group_id: 'group1' }] }]);
379
+ return parameter_sets;
380
+ }
381
+ });
382
+
383
+ expect(buckets.map((b) => b.bucket)).toEqual([bucketRequest(syncRules, 'mybucket["group1"]').bucket]);
326
384
  });
327
385
 
328
386
  test('save and load parameters with workspaceId', async () => {
@@ -342,7 +400,7 @@ bucket_definitions:
342
400
  }
343
401
  )
344
402
  );
345
- const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncRules();
403
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
346
404
  const bucketStorage = factory.getInstance(syncRules);
347
405
 
348
406
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -366,10 +424,10 @@ bucket_definitions:
366
424
 
367
425
  const buckets = await querier.queryDynamicBucketDescriptions({
368
426
  async getParameterSets(lookups) {
369
- expect(lookups).toEqual([ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '1'), ['u1'])]);
427
+ expect(lookups.map((l) => l.indexKey)).toEqual([['u1']]);
370
428
 
371
- const parameter_sets = await checkpoint.getParameterSets(lookups);
372
- expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }]);
429
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
430
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ workspace_id: 'workspace1' }] }]);
373
431
  return parameter_sets;
374
432
  }
375
433
  });
@@ -400,7 +458,7 @@ bucket_definitions:
400
458
  }
401
459
  )
402
460
  );
403
- const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncRules();
461
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
404
462
  const bucketStorage = factory.getInstance(syncRules);
405
463
 
406
464
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -446,11 +504,15 @@ bucket_definitions:
446
504
 
447
505
  const buckets = await querier.queryDynamicBucketDescriptions({
448
506
  async getParameterSets(lookups) {
449
- expect(lookups).toEqual([ScopedParameterLookup.direct(parameterLookupScope('by_public_workspace', '1'), [])]);
507
+ expect(lookups.map((l) => l.indexKey)).toEqual([[]]);
450
508
 
451
- const parameter_sets = await checkpoint.getParameterSets(lookups);
452
- parameter_sets.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
453
- expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
509
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
510
+ expect(parameter_sets).toHaveLength(1);
511
+
512
+ const [{ lookup, rows }] = parameter_sets;
513
+ expect(lookup).toEqual(lookups[0]);
514
+ rows.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
515
+ expect(rows).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
454
516
  return parameter_sets;
455
517
  }
456
518
  });
@@ -490,7 +552,7 @@ bucket_definitions:
490
552
  }
491
553
  )
492
554
  );
493
- const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncRules();
555
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
494
556
  const bucketStorage = factory.getInstance(syncRules);
495
557
 
496
558
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
@@ -554,16 +616,17 @@ bucket_definitions:
554
616
  await querier.queryDynamicBucketDescriptions({
555
617
  async getParameterSets(lookups) {
556
618
  foundLookups.push(...lookups);
557
- const output = await checkpoint.getParameterSets(lookups);
558
- parameter_sets.push(...output);
619
+ const output = await checkpoint.getParameterSets(lookups, 1000);
620
+ for (const { rows } of output) {
621
+ parameter_sets.push(...rows);
622
+ }
623
+
559
624
  return output;
560
625
  }
561
626
  })
562
627
  ).map((e) => e.bucket);
563
- expect(foundLookups).toEqual([
564
- ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '1'), []),
565
- ScopedParameterLookup.direct(parameterLookupScope('by_workspace', '2'), ['u1'])
566
- ]);
628
+ // Not testing the scope anymore - the exact format depends on storage version
629
+ expect(foundLookups.map((l) => l.indexKey)).toEqual([[], ['u1']]);
567
630
  parameter_sets.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
568
631
  expect(parameter_sets).toEqual([{ workspace_id: 'workspace1' }, { workspace_id: 'workspace3' }]);
569
632
 
@@ -591,6 +654,7 @@ bucket_definitions:
591
654
  )
592
655
  );
593
656
  const bucketStorage = factory.getInstance(syncRules);
657
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
594
658
 
595
659
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
596
660
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -611,9 +675,19 @@ bucket_definitions:
611
675
  await writer.flush();
612
676
 
613
677
  const checkpoint = await bucketStorage.getCheckpoint();
678
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
679
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
614
680
 
615
- const parameters = await checkpoint.getParameterSets([ScopedParameterLookup.direct(MYBUCKET_1, ['user1'])]);
616
- expect(parameters).toEqual([]);
681
+ const buckets = await querier.queryDynamicBucketDescriptions({
682
+ async getParameterSets(lookups) {
683
+ expect(lookups.map((l) => l.indexKey)).toEqual([['user1']]);
684
+
685
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
686
+ expect(parameter_sets).toEqual([]);
687
+ return parameter_sets;
688
+ }
689
+ });
690
+ expect(buckets).toEqual([]);
617
691
  });
618
692
 
619
693
  test('invalidate cached parsed sync rules', async () => {
@@ -671,6 +745,7 @@ streams:
671
745
  `)
672
746
  );
673
747
  const bucketStorage = factory.getInstance(syncRules);
748
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
674
749
 
675
750
  await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
676
751
  const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
@@ -688,13 +763,390 @@ streams:
688
763
  await writer.commit('1/1');
689
764
 
690
765
  const checkpoint = await bucketStorage.getCheckpoint();
691
- const parameters = await checkpoint.getParameterSets([
692
- ScopedParameterLookup.direct(parameterLookupScope('lookup', '0'), ['baz'])
766
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'baz' }), {});
767
+ const querier = sync_rules.getBucketParameterQuerier({
768
+ ...test_utils.querierOptions(parameters),
769
+ streams: {
770
+ stream: [
771
+ {
772
+ priorityOverride: null,
773
+ parameters: null,
774
+ opaque_id: 123
775
+ }
776
+ ]
777
+ }
778
+ }).querier;
779
+
780
+ const buckets = await querier.queryDynamicBucketDescriptions({
781
+ async getParameterSets(lookups) {
782
+ expect(lookups.map((l) => l.indexKey)).toEqual([['baz']]);
783
+
784
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
785
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'bar' }] }]);
786
+ return parameter_sets;
787
+ }
788
+ });
789
+ expect(buckets).toHaveLength(1);
790
+ expect(buckets).toMatchObject([
791
+ {
792
+ bucket: expect.stringMatching(/stream.*\["bar"\]$/),
793
+ definition: 'stream',
794
+ inclusion_reasons: [{ subscription: 123 }],
795
+ priority: 3
796
+ }
693
797
  ]);
694
- expect(parameters).toEqual([
798
+ });
799
+
800
+ test('respects parameter limit', async () => {
801
+ await using factory = await generateStorageFactory();
802
+ const syncRules = await factory.updateSyncRules(
803
+ updateSyncRulesFromYaml(
804
+ `
805
+ config:
806
+ edition: 3
807
+
808
+ streams:
809
+ a:
810
+ auto_subscribe: true
811
+ query: SELECT * FROM a WHERE id IN (SELECT id FROM b)
812
+ `,
813
+ {
814
+ storageVersion
815
+ }
816
+ )
817
+ );
818
+ const bucketStorage = factory.getInstance(syncRules);
819
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
820
+
821
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
822
+ const testTable = await test_utils.resolveTestTable(writer, 'b', ['id'], config);
823
+ await writer.markAllSnapshotDone('1/1');
824
+
825
+ for (let i = 0; i < 10; i++) {
826
+ await writer.save({
827
+ sourceTable: testTable,
828
+ tag: storage.SaveOperationTag.INSERT,
829
+ after: {
830
+ id: `t${i}`
831
+ },
832
+ afterReplicaId: test_utils.rid(`t${i}`)
833
+ });
834
+ }
835
+
836
+ await writer.commit('1/1');
837
+
838
+ const checkpoint = await bucketStorage.getCheckpoint();
839
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'u' }), {});
840
+ const querier = sync_rules.getBucketParameterQuerier(test_utils.querierOptions(parameters)).querier;
841
+
842
+ await expect(
843
+ querier.queryDynamicBucketDescriptions({
844
+ async getParameterSets(lookups) {
845
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 5);
846
+ return parameter_sets;
847
+ }
848
+ })
849
+ ).rejects.toThrow('Too many parameter results (limit was 5)');
850
+ });
851
+
852
+ test('sync streams store multiple parameter outputs for a single source row and lookup', async () => {
853
+ await using factory = await generateStorageFactory();
854
+ const syncRules = await factory.updateSyncRules(
855
+ updateSyncRulesFromYaml(`
856
+ config:
857
+ edition: 3
858
+ streams:
859
+ chat:
860
+ auto_subscribe: true
861
+ query: |
862
+ SELECT a.*
863
+ FROM a, b, json_each(b.x) x, json_each(b.y) y
864
+ WHERE a.x = x.value AND y.value = auth.user_id()
865
+ `)
866
+ );
867
+ const bucketStorage = factory.getInstance(syncRules);
868
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
869
+
870
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
871
+ const tableB = await test_utils.resolveTestTable(writer, 'b', ['id'], config);
872
+ const firstState = {
873
+ id: 'id0',
874
+ x: JSON.stringify(['x1', 'x2']),
875
+ y: JSON.stringify(['y1', 'y2'])
876
+ };
877
+ const secondState = {
878
+ id: 'id0',
879
+ x: JSON.stringify(['x2']),
880
+ y: JSON.stringify(['y1', 'y2'])
881
+ };
882
+ const thirdState = {
883
+ id: 'id0',
884
+ x: JSON.stringify(['x2']),
885
+ y: JSON.stringify(['y2'])
886
+ };
887
+ const replicaId = test_utils.rid('id0');
888
+
889
+ await writer.markAllSnapshotDone('1/1');
890
+ await writer.save({
891
+ sourceTable: tableB,
892
+ tag: storage.SaveOperationTag.INSERT,
893
+ after: firstState,
894
+ afterReplicaId: replicaId
895
+ });
896
+
897
+ await writer.commit('1/1');
898
+
899
+ let checkpoint = await bucketStorage.getCheckpoint();
900
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'y1' }), {});
901
+ const querier = sync_rules.getBucketParameterQuerier({
902
+ ...test_utils.querierOptions(parameters)
903
+ }).querier;
904
+
905
+ let buckets = await querier.queryDynamicBucketDescriptions({
906
+ async getParameterSets(lookups) {
907
+ expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
908
+
909
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
910
+
911
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'x1' }, { '0': 'x2' }] }]);
912
+ return parameter_sets;
913
+ }
914
+ });
915
+
916
+ expect(buckets).toMatchObject([
695
917
  {
696
- '0': 'bar'
918
+ bucket: expect.stringMatching(/chat.*\["x1"\]$/),
919
+ definition: 'chat',
920
+ inclusion_reasons: ['default'],
921
+ priority: 3
922
+ },
923
+ {
924
+ bucket: expect.stringMatching(/chat.*\["x2"\]$/),
925
+ definition: 'chat',
926
+ inclusion_reasons: ['default'],
927
+ priority: 3
928
+ }
929
+ ]);
930
+
931
+ // Make the x2 bucket inaccessible
932
+ await writer.save({
933
+ sourceTable: tableB,
934
+ tag: storage.SaveOperationTag.UPDATE,
935
+ before: firstState,
936
+ after: secondState,
937
+ beforeReplicaId: replicaId,
938
+ afterReplicaId: replicaId
939
+ });
940
+ await writer.commit('1/2');
941
+
942
+ checkpoint = await bucketStorage.getCheckpoint();
943
+ buckets = await querier.queryDynamicBucketDescriptions({
944
+ async getParameterSets(lookups) {
945
+ expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
946
+
947
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
948
+
949
+ expect(parameter_sets).toEqual([{ lookup: lookups[0], rows: [{ '0': 'x2' }] }]);
950
+ return parameter_sets;
951
+ }
952
+ });
953
+ expect(buckets.map((bkt) => bkt.bucket)).toStrictEqual([expect.stringContaining('x2')]);
954
+
955
+ // Second update, remove user from inputs
956
+ await writer.save({
957
+ sourceTable: tableB,
958
+ tag: storage.SaveOperationTag.UPDATE,
959
+ before: secondState,
960
+ after: thirdState,
961
+ beforeReplicaId: replicaId,
962
+ afterReplicaId: replicaId
963
+ });
964
+ await writer.commit('1/3');
965
+
966
+ checkpoint = await bucketStorage.getCheckpoint();
967
+ buckets = await querier.queryDynamicBucketDescriptions({
968
+ async getParameterSets(lookups) {
969
+ expect(lookups.map((l) => l.indexKey)).toEqual([['y1']]);
970
+
971
+ const parameter_sets = await checkpoint.getParameterSets(lookups, 1000);
972
+
973
+ expect(parameter_sets).toHaveLength(0);
974
+ return parameter_sets;
975
+ }
976
+ });
977
+ expect(buckets).toHaveLength(0);
978
+ });
979
+
980
+ test('can request multiple lookups at once', async () => {
981
+ await using factory = await generateStorageFactory();
982
+ const syncRules = await factory.updateSyncRules(
983
+ updateSyncRulesFromYaml(`
984
+ config:
985
+ edition: 3
986
+ streams:
987
+ a:
988
+ auto_subscribe: true
989
+ query: SELECT * FROM a WHERE p IN (SELECT id FROM param_a WHERE u = auth.user_id())
990
+ b:
991
+ auto_subscribe: true
992
+ query: SELECT * FROM b WHERE p IN (SELECT id FROM param_b WHERE u = auth.user_id())
993
+ `)
994
+ );
995
+ const bucketStorage = factory.getInstance(syncRules);
996
+ const parsedSyncRules = syncRules.parsed(test_utils.PARSE_OPTIONS);
997
+ const hydrationState = parsedSyncRules.hydrationState;
998
+ const syncConfig = parsedSyncRules.syncConfigWithErrors.config;
999
+
1000
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1001
+ const paramATable = await test_utils.resolveTestTable(writer, 'param_a', ['id'], config, 1);
1002
+ const paramBTable = await test_utils.resolveTestTable(writer, 'param_b', ['id'], config, 2);
1003
+ const replicaId = test_utils.rid('id');
1004
+
1005
+ // Insert the same row into param_a and param_b
1006
+ await writer.markAllSnapshotDone('1/1');
1007
+ await writer.save({
1008
+ sourceTable: paramATable,
1009
+ tag: storage.SaveOperationTag.INSERT,
1010
+ after: {
1011
+ id: 'id',
1012
+ u: 'user'
1013
+ },
1014
+ afterReplicaId: replicaId
1015
+ });
1016
+ await writer.save({
1017
+ sourceTable: paramBTable,
1018
+ tag: storage.SaveOperationTag.INSERT,
1019
+ after: {
1020
+ id: 'id',
1021
+ u: 'user'
1022
+ },
1023
+ afterReplicaId: replicaId
1024
+ });
1025
+ await writer.commit('1/1');
1026
+
1027
+ function findParameterOnTable(name: string): ParameterIndexLookupCreator {
1028
+ for (const source of syncConfig.bucketParameterLookupSources) {
1029
+ for (const param of source.getSourceTables()) {
1030
+ if (param.name == name) return source;
1031
+ }
1032
+ }
1033
+
1034
+ throw new Error(`Expected parameter index on ${name}`);
1035
+ }
1036
+
1037
+ // Run two lookups on separate parameter indexes.
1038
+ const lookupA = ScopedParameterLookup.normalized(
1039
+ hydrationState.getParameterIndexLookupScope(findParameterOnTable('param_a')),
1040
+ UnscopedParameterLookup.normalized(['user'])
1041
+ );
1042
+ const lookupB = ScopedParameterLookup.normalized(
1043
+ hydrationState.getParameterIndexLookupScope(findParameterOnTable('param_b')),
1044
+ UnscopedParameterLookup.normalized(['user'])
1045
+ );
1046
+
1047
+ const checkpoint = await bucketStorage.getCheckpoint();
1048
+ const parameterSets = await checkpoint.getParameterSets([lookupA, lookupB], 1000);
1049
+ const expectedRow = { '0': 'id' };
1050
+ let foundLookupA = false,
1051
+ foundLookupB = false;
1052
+
1053
+ // We should get the same row on both, with information on which lookup contributed which row.
1054
+ for (const { lookup, rows } of parameterSets) {
1055
+ if (lookup === lookupA) {
1056
+ foundLookupA = true;
1057
+ expect(rows).toStrictEqual([expectedRow]);
1058
+ } else if (lookup === lookupB) {
1059
+ foundLookupB = true;
1060
+ expect(rows).toStrictEqual([expectedRow]);
1061
+ } else {
1062
+ throw new Error('unexpected lookup in results');
1063
+ }
1064
+ }
1065
+
1066
+ expect(foundLookupA).toBeTruthy();
1067
+ expect(foundLookupB).toBeTruthy();
1068
+ });
1069
+
1070
+ test('sync streams preserve duplicate downstream lookups with different provenance', async () => {
1071
+ await using factory = await generateStorageFactory();
1072
+ const syncRules = await factory.updateSyncRules(
1073
+ updateSyncRulesFromYaml(
1074
+ `
1075
+ config:
1076
+ edition: 3
1077
+ streams:
1078
+ stream:
1079
+ auto_subscribe: true
1080
+ query: |
1081
+ SELECT a.*
1082
+ FROM a, b, c
1083
+ WHERE a.x = b.x
1084
+ AND a.z = c.z
1085
+ AND b.y = c.y
1086
+ AND c.u = auth.user_id()
1087
+ `,
1088
+ {
1089
+ storageVersion
1090
+ }
1091
+ )
1092
+ );
1093
+ const bucketStorage = factory.getInstance(syncRules);
1094
+ const sync_rules = syncRules.parsed(test_utils.PARSE_OPTIONS).hydratedSyncConfig();
1095
+
1096
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1097
+ const tableB = await test_utils.resolveTestTable(writer, 'b', ['id'], config, 1);
1098
+ const tableC = await test_utils.resolveTestTable(writer, 'c', ['id'], config, 2);
1099
+
1100
+ await writer.markAllSnapshotDone('1/1');
1101
+ await writer.save({
1102
+ sourceTable: tableB,
1103
+ tag: storage.SaveOperationTag.INSERT,
1104
+ after: {
1105
+ id: 'b1',
1106
+ y: 'shared-y',
1107
+ x: 'x-from-shared-y'
1108
+ },
1109
+ afterReplicaId: test_utils.rid('b1')
1110
+ });
1111
+ await writer.save({
1112
+ sourceTable: tableC,
1113
+ tag: storage.SaveOperationTag.INSERT,
1114
+ after: {
1115
+ id: 'c1',
1116
+ u: 'user1',
1117
+ y: 'shared-y',
1118
+ z: 'z1'
1119
+ },
1120
+ afterReplicaId: test_utils.rid('c1')
1121
+ });
1122
+ await writer.save({
1123
+ sourceTable: tableC,
1124
+ tag: storage.SaveOperationTag.INSERT,
1125
+ after: {
1126
+ id: 'c2',
1127
+ u: 'user1',
1128
+ y: 'shared-y',
1129
+ z: 'z2'
1130
+ },
1131
+ afterReplicaId: test_utils.rid('c2')
1132
+ });
1133
+ await writer.commit('1/1');
1134
+
1135
+ const checkpoint = await bucketStorage.getCheckpoint();
1136
+ const parameters = new RequestParameters(new JwtPayload({ sub: 'user1' }), {});
1137
+ const querier = sync_rules.getBucketParameterQuerier({
1138
+ ...test_utils.querierOptions(parameters)
1139
+ }).querier;
1140
+
1141
+ const buckets = await querier.queryDynamicBucketDescriptions({
1142
+ async getParameterSets(lookups) {
1143
+ return checkpoint.getParameterSets(lookups, 1000);
697
1144
  }
1145
+ });
1146
+
1147
+ expect(buckets.map((bucket) => bucket.bucket).sort()).toStrictEqual([
1148
+ expect.stringMatching(/stream.*\["x-from-shared-y","z1"\]$/),
1149
+ expect.stringMatching(/stream.*\["x-from-shared-y","z2"\]$/)
698
1150
  ]);
699
1151
  });
700
1152
  }