@powersync/service-core-tests 0.14.0 → 0.15.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 (35) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/test-utils/general-utils.d.ts +9 -2
  3. package/dist/test-utils/general-utils.js +26 -2
  4. package/dist/test-utils/general-utils.js.map +1 -1
  5. package/dist/tests/register-compacting-tests.d.ts +1 -1
  6. package/dist/tests/register-compacting-tests.js +122 -68
  7. package/dist/tests/register-compacting-tests.js.map +1 -1
  8. package/dist/tests/register-data-storage-checkpoint-tests.d.ts +1 -1
  9. package/dist/tests/register-data-storage-checkpoint-tests.js +38 -6
  10. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  11. package/dist/tests/register-data-storage-data-tests.d.ts +2 -2
  12. package/dist/tests/register-data-storage-data-tests.js +666 -142
  13. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  14. package/dist/tests/register-data-storage-parameter-tests.d.ts +1 -1
  15. package/dist/tests/register-data-storage-parameter-tests.js +60 -33
  16. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  17. package/dist/tests/register-parameter-compacting-tests.d.ts +1 -1
  18. package/dist/tests/register-parameter-compacting-tests.js +8 -4
  19. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  20. package/dist/tests/register-sync-tests.d.ts +2 -1
  21. package/dist/tests/register-sync-tests.js +40 -18
  22. package/dist/tests/register-sync-tests.js.map +1 -1
  23. package/dist/tests/util.d.ts +5 -4
  24. package/dist/tests/util.js +27 -12
  25. package/dist/tests/util.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/test-utils/general-utils.ts +41 -3
  28. package/src/tests/register-compacting-tests.ts +127 -82
  29. package/src/tests/register-data-storage-checkpoint-tests.ts +64 -11
  30. package/src/tests/register-data-storage-data-tests.ts +640 -52
  31. package/src/tests/register-data-storage-parameter-tests.ts +101 -47
  32. package/src/tests/register-parameter-compacting-tests.ts +9 -4
  33. package/src/tests/register-sync-tests.ts +45 -19
  34. package/src/tests/util.ts +46 -17
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -1,13 +1,16 @@
1
1
  import {
2
2
  BucketDataBatchOptions,
3
+ CURRENT_STORAGE_VERSION,
3
4
  getUuidReplicaIdentityBson,
4
5
  OplogEntry,
6
+ reduceBucket,
5
7
  storage,
6
8
  updateSyncRulesFromYaml
7
9
  } from '@powersync/service-core';
8
10
  import { describe, expect, test } from 'vitest';
9
11
  import * as test_utils from '../test-utils/test-utils-index.js';
10
- import { bucketRequest, bucketRequestMap, bucketRequests, TEST_TABLE } from './util.js';
12
+ import { bucketRequest } from '../test-utils/test-utils-index.js';
13
+ import { bucketRequestMap, bucketRequests } from './util.js';
11
14
 
12
15
  /**
13
16
  * Normalize data from OplogEntries for comparison in tests.
@@ -30,21 +33,30 @@ const normalizeOplogData = (data: OplogEntry['data']) => {
30
33
  *
31
34
  * ```
32
35
  */
33
- export function registerDataStorageDataTests(generateStorageFactory: storage.TestStorageFactory) {
36
+ export function registerDataStorageDataTests(config: storage.TestStorageConfig) {
37
+ const generateStorageFactory = config.factory;
38
+ const storageVersion = config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
39
+
40
+ const TEST_TABLE = test_utils.makeTestTable('test', ['id'], config);
41
+
34
42
  test('removing row', async () => {
35
43
  await using factory = await generateStorageFactory();
36
44
  const syncRules = await factory.updateSyncRules(
37
- updateSyncRulesFromYaml(`
45
+ updateSyncRulesFromYaml(
46
+ `
38
47
  bucket_definitions:
39
48
  global:
40
49
  data:
41
50
  - SELECT id, description FROM "%"
42
- `)
51
+ `,
52
+ { storageVersion }
53
+ )
43
54
  );
44
55
  const bucketStorage = factory.getInstance(syncRules);
45
56
 
46
57
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
47
58
  const sourceTable = TEST_TABLE;
59
+ await batch.markAllSnapshotDone('1/1');
48
60
 
49
61
  await batch.save({
50
62
  sourceTable,
@@ -89,27 +101,327 @@ bucket_definitions:
89
101
  ];
90
102
  expect(checksums).toEqual([
91
103
  {
92
- bucket: bucketRequest(syncRules, 'global[]'),
104
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
93
105
  checksum: (c1 + c2) & 0xffffffff,
94
106
  count: 2
95
107
  }
96
108
  ]);
97
109
  });
98
110
 
111
+ test('insert after delete in new batch', async () => {
112
+ await using factory = await generateStorageFactory();
113
+ const syncRules = await factory.updateSyncRules(
114
+ updateSyncRulesFromYaml(
115
+ `
116
+ bucket_definitions:
117
+ global:
118
+ data:
119
+ - SELECT id, description FROM "%"
120
+ `,
121
+ { storageVersion }
122
+ )
123
+ );
124
+ const bucketStorage = factory.getInstance(syncRules);
125
+
126
+ const sourceTable = TEST_TABLE;
127
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
128
+ await batch.markAllSnapshotDone('1/1');
129
+
130
+ await batch.save({
131
+ sourceTable,
132
+ tag: storage.SaveOperationTag.DELETE,
133
+ beforeReplicaId: test_utils.rid('test1')
134
+ });
135
+
136
+ await batch.commit('0/1');
137
+ });
138
+
139
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
140
+ const sourceTable = TEST_TABLE;
141
+
142
+ await batch.save({
143
+ sourceTable,
144
+ tag: storage.SaveOperationTag.INSERT,
145
+ after: {
146
+ id: 'test1',
147
+ description: 'test1'
148
+ },
149
+ afterReplicaId: test_utils.rid('test1')
150
+ });
151
+ await batch.commit('2/1');
152
+ });
153
+
154
+ const { checkpoint } = await bucketStorage.getCheckpoint();
155
+
156
+ const batch = await test_utils.fromAsync(
157
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
158
+ );
159
+ const data = batch[0].chunkData.data.map((d) => {
160
+ return {
161
+ op: d.op,
162
+ object_id: d.object_id,
163
+ checksum: d.checksum
164
+ };
165
+ });
166
+
167
+ const c1 = 2871785649;
168
+
169
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
170
+
171
+ const checksums = [
172
+ ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
173
+ ];
174
+ expect(checksums).toEqual([
175
+ {
176
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
177
+ checksum: c1 & 0xffffffff,
178
+ count: 1
179
+ }
180
+ ]);
181
+ });
182
+
183
+ test('update after delete in new batch', async () => {
184
+ // Update after delete may not be common, but the storage layer should handle it in an eventually-consistent way.
185
+ await using factory = await generateStorageFactory();
186
+ const syncRules = await factory.updateSyncRules(
187
+ updateSyncRulesFromYaml(
188
+ `
189
+ bucket_definitions:
190
+ global:
191
+ data:
192
+ - SELECT id, description FROM "%"
193
+ `,
194
+ { storageVersion }
195
+ )
196
+ );
197
+ const bucketStorage = factory.getInstance(syncRules);
198
+
199
+ const sourceTable = TEST_TABLE;
200
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
201
+ await batch.markAllSnapshotDone('1/1');
202
+
203
+ await batch.save({
204
+ sourceTable,
205
+ tag: storage.SaveOperationTag.DELETE,
206
+ beforeReplicaId: test_utils.rid('test1')
207
+ });
208
+
209
+ await batch.commit('0/1');
210
+ });
211
+
212
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
213
+ const sourceTable = TEST_TABLE;
214
+
215
+ await batch.save({
216
+ sourceTable,
217
+ tag: storage.SaveOperationTag.UPDATE,
218
+ before: {
219
+ id: 'test1'
220
+ },
221
+ after: {
222
+ id: 'test1',
223
+ description: 'test1'
224
+ },
225
+ beforeReplicaId: test_utils.rid('test1'),
226
+ afterReplicaId: test_utils.rid('test1')
227
+ });
228
+ await batch.commit('2/1');
229
+ });
230
+
231
+ const { checkpoint } = await bucketStorage.getCheckpoint();
232
+
233
+ const batch = await test_utils.fromAsync(
234
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
235
+ );
236
+ const data = batch[0].chunkData.data.map((d) => {
237
+ return {
238
+ op: d.op,
239
+ object_id: d.object_id,
240
+ checksum: d.checksum
241
+ };
242
+ });
243
+
244
+ const c1 = 2871785649;
245
+
246
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
247
+
248
+ const checksums = [
249
+ ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
250
+ ];
251
+ expect(checksums).toEqual([
252
+ {
253
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
254
+ checksum: c1 & 0xffffffff,
255
+ count: 1
256
+ }
257
+ ]);
258
+ });
259
+
260
+ test('insert after delete in same batch', async () => {
261
+ await using factory = await generateStorageFactory();
262
+ const syncRules = await factory.updateSyncRules(
263
+ updateSyncRulesFromYaml(
264
+ `
265
+ bucket_definitions:
266
+ global:
267
+ data:
268
+ - SELECT id, description FROM "%"
269
+ `,
270
+ {
271
+ storageVersion
272
+ }
273
+ )
274
+ );
275
+ const bucketStorage = factory.getInstance(syncRules);
276
+
277
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
278
+ const sourceTable = TEST_TABLE;
279
+ await batch.markAllSnapshotDone('1/1');
280
+
281
+ await batch.save({
282
+ sourceTable,
283
+ tag: storage.SaveOperationTag.DELETE,
284
+ beforeReplicaId: test_utils.rid('test1')
285
+ });
286
+ await batch.save({
287
+ sourceTable,
288
+ tag: storage.SaveOperationTag.INSERT,
289
+ after: {
290
+ id: 'test1',
291
+ description: 'test1'
292
+ },
293
+ afterReplicaId: test_utils.rid('test1')
294
+ });
295
+ await batch.commit('1/1');
296
+ });
297
+
298
+ const { checkpoint } = await bucketStorage.getCheckpoint();
299
+
300
+ const batch = await test_utils.fromAsync(
301
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
302
+ );
303
+ const data = batch[0].chunkData.data.map((d) => {
304
+ return {
305
+ op: d.op,
306
+ object_id: d.object_id,
307
+ checksum: d.checksum
308
+ };
309
+ });
310
+
311
+ const c1 = 2871785649;
312
+
313
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
314
+
315
+ const checksums = [
316
+ ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
317
+ ];
318
+ expect(checksums).toEqual([
319
+ {
320
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
321
+ checksum: c1 & 0xffffffff,
322
+ count: 1
323
+ }
324
+ ]);
325
+ });
326
+
327
+ test('(insert, delete, insert), (delete)', async () => {
328
+ await using factory = await generateStorageFactory();
329
+ const syncRules = await factory.updateSyncRules(
330
+ updateSyncRulesFromYaml(
331
+ `
332
+ bucket_definitions:
333
+ global:
334
+ data:
335
+ - SELECT id, description FROM "%"
336
+ `,
337
+ {
338
+ storageVersion
339
+ }
340
+ )
341
+ );
342
+ const bucketStorage = factory.getInstance(syncRules);
343
+
344
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
345
+ const sourceTable = TEST_TABLE;
346
+ await batch.markAllSnapshotDone('1/1');
347
+
348
+ await batch.save({
349
+ sourceTable,
350
+ tag: storage.SaveOperationTag.INSERT,
351
+ after: {
352
+ id: 'test1',
353
+ description: 'test1'
354
+ },
355
+ afterReplicaId: test_utils.rid('test1')
356
+ });
357
+ await batch.save({
358
+ sourceTable,
359
+ tag: storage.SaveOperationTag.DELETE,
360
+ beforeReplicaId: test_utils.rid('test1')
361
+ });
362
+ await batch.save({
363
+ sourceTable,
364
+ tag: storage.SaveOperationTag.INSERT,
365
+ after: {
366
+ id: 'test1',
367
+ description: 'test1'
368
+ },
369
+ afterReplicaId: test_utils.rid('test1')
370
+ });
371
+ await batch.commit('1/1');
372
+ });
373
+
374
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
375
+ const sourceTable = TEST_TABLE;
376
+ await batch.markAllSnapshotDone('1/1');
377
+
378
+ await batch.save({
379
+ sourceTable,
380
+ tag: storage.SaveOperationTag.DELETE,
381
+ beforeReplicaId: test_utils.rid('test1')
382
+ });
383
+ await batch.commit('2/1');
384
+ });
385
+
386
+ const { checkpoint } = await bucketStorage.getCheckpoint();
387
+
388
+ const batch = await test_utils.fromAsync(
389
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
390
+ );
391
+
392
+ expect(reduceBucket(batch[0].chunkData.data).slice(1)).toEqual([]);
393
+
394
+ const data = batch[0].chunkData.data.map((d) => {
395
+ return {
396
+ op: d.op,
397
+ object_id: d.object_id,
398
+ checksum: d.checksum
399
+ };
400
+ });
401
+
402
+ expect(data).toMatchSnapshot();
403
+ });
404
+
99
405
  test('changing client ids', async () => {
100
406
  await using factory = await generateStorageFactory();
101
407
  const syncRules = await factory.updateSyncRules(
102
- updateSyncRulesFromYaml(`
408
+ updateSyncRulesFromYaml(
409
+ `
103
410
  bucket_definitions:
104
411
  global:
105
412
  data:
106
413
  - SELECT client_id as id, description FROM "%"
107
- `)
414
+ `,
415
+ {
416
+ storageVersion
417
+ }
418
+ )
108
419
  );
109
420
  const bucketStorage = factory.getInstance(syncRules);
110
421
 
111
422
  const sourceTable = TEST_TABLE;
112
423
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
424
+ await batch.markAllSnapshotDone('1/1');
113
425
  await batch.save({
114
426
  sourceTable,
115
427
  tag: storage.SaveOperationTag.INSERT,
@@ -166,17 +478,23 @@ bucket_definitions:
166
478
  test('re-apply delete', async () => {
167
479
  await using factory = await generateStorageFactory();
168
480
  const syncRules = await factory.updateSyncRules(
169
- updateSyncRulesFromYaml(`
481
+ updateSyncRulesFromYaml(
482
+ `
170
483
  bucket_definitions:
171
484
  global:
172
485
  data:
173
486
  - SELECT id, description FROM "%"
174
- `)
487
+ `,
488
+ {
489
+ storageVersion
490
+ }
491
+ )
175
492
  );
176
493
  const bucketStorage = factory.getInstance(syncRules);
177
494
 
178
495
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
179
496
  const sourceTable = TEST_TABLE;
497
+ await batch.markAllSnapshotDone('1/1');
180
498
 
181
499
  await batch.save({
182
500
  sourceTable,
@@ -237,7 +555,7 @@ bucket_definitions:
237
555
  ];
238
556
  expect(checksums).toEqual([
239
557
  {
240
- bucket: bucketRequest(syncRules, 'global[]'),
558
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
241
559
  checksum: (c1 + c2) & 0xffffffff,
242
560
  count: 2
243
561
  }
@@ -247,16 +565,20 @@ bucket_definitions:
247
565
  test('re-apply update + delete', async () => {
248
566
  await using factory = await generateStorageFactory();
249
567
  const syncRules = await factory.updateSyncRules(
250
- updateSyncRulesFromYaml(`
568
+ updateSyncRulesFromYaml(
569
+ `
251
570
  bucket_definitions:
252
571
  global:
253
572
  data:
254
573
  - SELECT id, description FROM "%"
255
- `)
574
+ `,
575
+ { storageVersion }
576
+ )
256
577
  );
257
578
  const bucketStorage = factory.getInstance(syncRules);
258
579
 
259
580
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
581
+ await batch.markAllSnapshotDone('1/1');
260
582
  const sourceTable = TEST_TABLE;
261
583
 
262
584
  await batch.save({
@@ -271,6 +593,7 @@ bucket_definitions:
271
593
  });
272
594
 
273
595
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
596
+ await batch.markAllSnapshotDone('1/1');
274
597
  const sourceTable = TEST_TABLE;
275
598
 
276
599
  await batch.save({
@@ -303,6 +626,7 @@ bucket_definitions:
303
626
  });
304
627
 
305
628
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
629
+ await batch.markAllSnapshotDone('1/1');
306
630
  const sourceTable = TEST_TABLE;
307
631
 
308
632
  await batch.save({
@@ -363,7 +687,7 @@ bucket_definitions:
363
687
  ];
364
688
  expect(checksums).toEqual([
365
689
  {
366
- bucket: bucketRequest(syncRules, 'global[]'),
690
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
367
691
  checksum: (c1 + c1 + c1 + c2) & 0xffffffff,
368
692
  count: 4
369
693
  }
@@ -381,17 +705,21 @@ bucket_definitions:
381
705
 
382
706
  await using factory = await generateStorageFactory();
383
707
  const syncRules = await factory.updateSyncRules(
384
- updateSyncRulesFromYaml(`
708
+ updateSyncRulesFromYaml(
709
+ `
385
710
  bucket_definitions:
386
711
  global:
387
712
  data:
388
713
  - SELECT id, description FROM "test"
389
- `)
714
+ `,
715
+ { storageVersion }
716
+ )
390
717
  );
391
718
  const bucketStorage = factory.getInstance(syncRules);
392
719
 
393
720
  // Pre-setup
394
721
  const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
722
+ await batch.markAllSnapshotDone('1/1');
395
723
  const sourceTable = TEST_TABLE;
396
724
 
397
725
  await batch.save({
@@ -539,19 +867,25 @@ bucket_definitions:
539
867
  }
540
868
  await using factory = await generateStorageFactory();
541
869
  const syncRules = await factory.updateSyncRules(
542
- updateSyncRulesFromYaml(`
870
+ updateSyncRulesFromYaml(
871
+ `
543
872
  bucket_definitions:
544
873
  global:
545
874
  data:
546
875
  - SELECT id, description FROM "test"
547
- `)
876
+ `,
877
+ {
878
+ storageVersion
879
+ }
880
+ )
548
881
  );
549
882
  const bucketStorage = factory.getInstance(syncRules);
550
883
 
551
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
884
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
552
885
 
553
886
  // Pre-setup
554
887
  const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
888
+ await batch.markAllSnapshotDone('1/1');
555
889
  await batch.save({
556
890
  sourceTable,
557
891
  tag: storage.SaveOperationTag.INSERT,
@@ -647,19 +981,25 @@ bucket_definitions:
647
981
 
648
982
  await using factory = await generateStorageFactory();
649
983
  const syncRules = await factory.updateSyncRules(
650
- updateSyncRulesFromYaml(`
984
+ updateSyncRulesFromYaml(
985
+ `
651
986
  bucket_definitions:
652
987
  global:
653
988
  data:
654
989
  - SELECT id, description FROM "test"
655
- `)
990
+ `,
991
+ {
992
+ storageVersion
993
+ }
994
+ )
656
995
  );
657
996
  const bucketStorage = factory.getInstance(syncRules);
658
997
 
659
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
998
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
660
999
 
661
1000
  // Pre-setup
662
1001
  const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1002
+ await batch.markAllSnapshotDone('1/1');
663
1003
  await batch.save({
664
1004
  sourceTable,
665
1005
  tag: storage.SaveOperationTag.INSERT,
@@ -745,16 +1085,22 @@ bucket_definitions:
745
1085
  // and the test will have to updated when other implementations are added.
746
1086
  await using factory = await generateStorageFactory();
747
1087
  const syncRules = await factory.updateSyncRules(
748
- updateSyncRulesFromYaml(`
1088
+ updateSyncRulesFromYaml(
1089
+ `
749
1090
  bucket_definitions:
750
1091
  global:
751
1092
  data:
752
1093
  - SELECT id, description FROM "%"
753
- `)
1094
+ `,
1095
+ {
1096
+ storageVersion
1097
+ }
1098
+ )
754
1099
  );
755
1100
  const bucketStorage = factory.getInstance(syncRules);
756
1101
 
757
1102
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1103
+ await batch.markAllSnapshotDone('1/1');
758
1104
  const sourceTable = TEST_TABLE;
759
1105
 
760
1106
  const largeDescription = '0123456789'.repeat(12_000_00);
@@ -854,16 +1200,22 @@ bucket_definitions:
854
1200
  // Test syncing a batch of data that is limited by count.
855
1201
  await using factory = await generateStorageFactory();
856
1202
  const syncRules = await factory.updateSyncRules(
857
- updateSyncRulesFromYaml(`
1203
+ updateSyncRulesFromYaml(
1204
+ `
858
1205
  bucket_definitions:
859
1206
  global:
860
1207
  data:
861
1208
  - SELECT id, description FROM "%"
862
- `)
1209
+ `,
1210
+ {
1211
+ storageVersion
1212
+ }
1213
+ )
863
1214
  );
864
1215
  const bucketStorage = factory.getInstance(syncRules);
865
1216
 
866
1217
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1218
+ await batch.markAllSnapshotDone('1/1');
867
1219
  const sourceTable = TEST_TABLE;
868
1220
 
869
1221
  for (let i = 1; i <= 6; i++) {
@@ -938,7 +1290,8 @@ bucket_definitions:
938
1290
  const setup = async (options: BucketDataBatchOptions) => {
939
1291
  await using factory = await generateStorageFactory();
940
1292
  const syncRules = await factory.updateSyncRules(
941
- updateSyncRulesFromYaml(`
1293
+ updateSyncRulesFromYaml(
1294
+ `
942
1295
  bucket_definitions:
943
1296
  global1:
944
1297
  data:
@@ -946,11 +1299,14 @@ bucket_definitions:
946
1299
  global2:
947
1300
  data:
948
1301
  - SELECT id, description FROM test WHERE bucket = 'global2'
949
- `)
1302
+ `,
1303
+ { storageVersion }
1304
+ )
950
1305
  );
951
1306
  const bucketStorage = factory.getInstance(syncRules);
952
1307
 
953
1308
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1309
+ await batch.markAllSnapshotDone('1/1');
954
1310
  const sourceTable = TEST_TABLE;
955
1311
 
956
1312
  for (let i = 1; i <= 10; i++) {
@@ -988,8 +1344,8 @@ bucket_definitions:
988
1344
  const { batch, syncRules } = await setup({ limit: 5 });
989
1345
  expect(batch.length).toEqual(2);
990
1346
 
991
- expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]'));
992
- expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1347
+ expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]').bucket);
1348
+ expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]').bucket);
993
1349
 
994
1350
  expect(test_utils.getBatchData(batch[0])).toEqual([
995
1351
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1019,8 +1375,8 @@ bucket_definitions:
1019
1375
  const { batch, syncRules } = await setup({ limit: 11 });
1020
1376
  expect(batch.length).toEqual(2);
1021
1377
 
1022
- expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]'));
1023
- expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1378
+ expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]').bucket);
1379
+ expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]').bucket);
1024
1380
 
1025
1381
  expect(test_utils.getBatchData(batch[0])).toEqual([
1026
1382
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1056,9 +1412,9 @@ bucket_definitions:
1056
1412
  const { batch, syncRules } = await setup({ limit: 3, chunkLimitBytes: 50 });
1057
1413
 
1058
1414
  expect(batch.length).toEqual(3);
1059
- expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]'));
1060
- expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1061
- expect(batch[2].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1415
+ expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]').bucket);
1416
+ expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]').bucket);
1417
+ expect(batch[2].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]').bucket);
1062
1418
 
1063
1419
  expect(test_utils.getBatchData(batch[0])).toEqual([
1064
1420
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1103,6 +1459,7 @@ bucket_definitions:
1103
1459
  const r = await f.configureSyncRules(updateSyncRulesFromYaml('bucket_definitions: {}'));
1104
1460
  const storage = f.getInstance(r.persisted_sync_rules!);
1105
1461
  await storage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1462
+ await batch.markAllSnapshotDone('1/0');
1106
1463
  await batch.keepalive('1/0');
1107
1464
  });
1108
1465
 
@@ -1116,20 +1473,26 @@ bucket_definitions:
1116
1473
  // Similar to the above test, but splits over 1MB chunks.
1117
1474
  await using factory = await generateStorageFactory();
1118
1475
  const syncRules = await factory.updateSyncRules(
1119
- updateSyncRulesFromYaml(`
1476
+ updateSyncRulesFromYaml(
1477
+ `
1120
1478
  bucket_definitions:
1121
1479
  global:
1122
1480
  data:
1123
1481
  - SELECT id FROM test
1124
1482
  - SELECT id FROM test_ignore WHERE false
1125
- `)
1483
+ `,
1484
+ {
1485
+ storageVersion
1486
+ }
1487
+ )
1126
1488
  );
1127
1489
  const bucketStorage = factory.getInstance(syncRules);
1128
1490
 
1129
- const sourceTable = test_utils.makeTestTable('test', ['id']);
1130
- const sourceTableIgnore = test_utils.makeTestTable('test_ignore', ['id']);
1491
+ const sourceTable = test_utils.makeTestTable('test', ['id'], config);
1492
+ const sourceTableIgnore = test_utils.makeTestTable('test_ignore', ['id'], config);
1131
1493
 
1132
1494
  const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1495
+ await batch.markAllSnapshotDone('1/1');
1133
1496
  // This saves a record to current_data, but not bucket_data.
1134
1497
  // This causes a checkpoint to be created without increasing the op_id sequence.
1135
1498
  await batch.save({
@@ -1163,17 +1526,23 @@ bucket_definitions:
1163
1526
  test('unchanged checksums', async () => {
1164
1527
  await using factory = await generateStorageFactory();
1165
1528
  const syncRules = await factory.updateSyncRules(
1166
- updateSyncRulesFromYaml(`
1529
+ updateSyncRulesFromYaml(
1530
+ `
1167
1531
  bucket_definitions:
1168
1532
  global:
1169
1533
  data:
1170
1534
  - SELECT client_id as id, description FROM "%"
1171
- `)
1535
+ `,
1536
+ {
1537
+ storageVersion
1538
+ }
1539
+ )
1172
1540
  );
1173
1541
  const bucketStorage = factory.getInstance(syncRules);
1174
1542
 
1175
1543
  const sourceTable = TEST_TABLE;
1176
1544
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1545
+ await batch.markAllSnapshotDone('1/1');
1177
1546
  await batch.save({
1178
1547
  sourceTable,
1179
1548
  tag: storage.SaveOperationTag.INSERT,
@@ -1190,14 +1559,226 @@ bucket_definitions:
1190
1559
  const checksums = [
1191
1560
  ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
1192
1561
  ];
1193
- expect(checksums).toEqual([{ bucket: bucketRequest(syncRules, 'global[]'), checksum: 1917136889, count: 1 }]);
1562
+ expect(checksums).toEqual([
1563
+ { bucket: bucketRequest(syncRules, 'global[]').bucket, checksum: 1917136889, count: 1 }
1564
+ ]);
1194
1565
  const checksums2 = [
1195
1566
  ...(await bucketStorage.getChecksums(checkpoint + 1n, bucketRequests(syncRules, ['global[]']))).values()
1196
1567
  ];
1197
- expect(checksums2).toEqual([{ bucket: bucketRequest(syncRules, 'global[]'), checksum: 1917136889, count: 1 }]);
1568
+ expect(checksums2).toEqual([
1569
+ { bucket: bucketRequest(syncRules, 'global[]').bucket, checksum: 1917136889, count: 1 }
1570
+ ]);
1198
1571
  });
1199
1572
 
1200
- testChecksumBatching(generateStorageFactory);
1573
+ testChecksumBatching(config);
1574
+
1575
+ test('empty checkpoints (1)', async () => {
1576
+ await using factory = await generateStorageFactory();
1577
+ const syncRules = await factory.updateSyncRules(
1578
+ updateSyncRulesFromYaml(
1579
+ `
1580
+ bucket_definitions:
1581
+ global:
1582
+ data:
1583
+ - SELECT id, description FROM "%"
1584
+ `,
1585
+ { storageVersion }
1586
+ )
1587
+ );
1588
+ const bucketStorage = factory.getInstance(syncRules);
1589
+
1590
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1591
+ await batch.markAllSnapshotDone('1/1');
1592
+ await batch.commit('1/1');
1593
+
1594
+ const cp1 = await bucketStorage.getCheckpoint();
1595
+ expect(cp1.lsn).toEqual('1/1');
1596
+
1597
+ await batch.commit('2/1', { createEmptyCheckpoints: true });
1598
+ const cp2 = await bucketStorage.getCheckpoint();
1599
+ expect(cp2.lsn).toEqual('2/1');
1600
+
1601
+ await batch.keepalive('3/1');
1602
+ const cp3 = await bucketStorage.getCheckpoint();
1603
+ expect(cp3.lsn).toEqual('3/1');
1604
+
1605
+ // For the last one, we skip creating empty checkpoints
1606
+ // This means the LSN stays at 3/1.
1607
+ await batch.commit('4/1', { createEmptyCheckpoints: false });
1608
+ const cp4 = await bucketStorage.getCheckpoint();
1609
+ expect(cp4.lsn).toEqual('3/1');
1610
+ });
1611
+ });
1612
+
1613
+ test('empty checkpoints (2)', async () => {
1614
+ await using factory = await generateStorageFactory();
1615
+ const syncRules = await factory.updateSyncRules(
1616
+ updateSyncRulesFromYaml(
1617
+ `
1618
+ bucket_definitions:
1619
+ global:
1620
+ data:
1621
+ - SELECT id, description FROM "%"
1622
+ `,
1623
+ {
1624
+ storageVersion
1625
+ }
1626
+ )
1627
+ );
1628
+ const bucketStorage = factory.getInstance(syncRules);
1629
+
1630
+ const sourceTable = TEST_TABLE;
1631
+ // We simulate two concurrent batches, but nesting is the easiest way to do this.
1632
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch1) => {
1633
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch2) => {
1634
+ await batch1.markAllSnapshotDone('1/1');
1635
+ await batch1.commit('1/1');
1636
+
1637
+ await batch1.commit('2/1', { createEmptyCheckpoints: false });
1638
+ const cp2 = await bucketStorage.getCheckpoint();
1639
+ expect(cp2.lsn).toEqual('1/1'); // checkpoint 2/1 skipped
1640
+
1641
+ await batch2.save({
1642
+ sourceTable,
1643
+ tag: storage.SaveOperationTag.INSERT,
1644
+ after: {
1645
+ id: 'test1',
1646
+ description: 'test1a'
1647
+ },
1648
+ afterReplicaId: test_utils.rid('test1')
1649
+ });
1650
+ // This simulates what happens on a snapshot processor.
1651
+ // This may later change to a flush() rather than commit().
1652
+ await batch2.commit(test_utils.BATCH_OPTIONS.zeroLSN);
1653
+
1654
+ const cp3 = await bucketStorage.getCheckpoint();
1655
+ expect(cp3.lsn).toEqual('1/1'); // Still unchanged
1656
+
1657
+ // This now needs to advance the LSN, despite {createEmptyCheckpoints: false}
1658
+ await batch1.commit('4/1', { createEmptyCheckpoints: false });
1659
+ const cp4 = await bucketStorage.getCheckpoint();
1660
+ expect(cp4.lsn).toEqual('4/1');
1661
+ });
1662
+ });
1663
+ });
1664
+
1665
+ test('empty checkpoints (sync rule activation)', async () => {
1666
+ await using factory = await generateStorageFactory();
1667
+ const syncRules = await factory.updateSyncRules(
1668
+ updateSyncRulesFromYaml(
1669
+ `
1670
+ bucket_definitions:
1671
+ global:
1672
+ data:
1673
+ - SELECT id, description FROM "%"
1674
+ `,
1675
+ {
1676
+ storageVersion
1677
+ }
1678
+ )
1679
+ );
1680
+ const bucketStorage = factory.getInstance(syncRules);
1681
+
1682
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1683
+ const result = await batch.commit('1/1', { createEmptyCheckpoints: false });
1684
+ expect(result).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1685
+ // Snapshot is only valid once we reach 3/1
1686
+ await batch.markAllSnapshotDone('3/1');
1687
+ });
1688
+
1689
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1690
+ // 2/1 < 3/1 - snapshot not valid yet, block checkpoint
1691
+ const result = await batch.commit('2/1', { createEmptyCheckpoints: false });
1692
+ expect(result).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1693
+ });
1694
+
1695
+ // No empty checkpoint should be created by the commit above.
1696
+ const cp1 = await bucketStorage.getCheckpoint();
1697
+ expect(cp1.lsn).toEqual(null);
1698
+
1699
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1700
+ // After this commit, the snapshot should be valid.
1701
+ // We specifically check that this is done even if createEmptyCheckpoints: false.
1702
+ const result = await batch.commit('3/1', { createEmptyCheckpoints: false });
1703
+ expect(result).toEqual({ checkpointBlocked: false, checkpointCreated: true });
1704
+ });
1705
+
1706
+ // Now, the checkpoint should advance the sync rules active.
1707
+ const cp2 = await bucketStorage.getCheckpoint();
1708
+ expect(cp2.lsn).toEqual('3/1');
1709
+
1710
+ const activeSyncRules = await factory.getActiveSyncRulesContent();
1711
+ expect(activeSyncRules?.id).toEqual(syncRules.id);
1712
+
1713
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1714
+ // At this point, it should be a truely empty checkpoint
1715
+ const result = await batch.commit('4/1', { createEmptyCheckpoints: false });
1716
+ expect(result).toEqual({ checkpointBlocked: false, checkpointCreated: false });
1717
+ });
1718
+
1719
+ // Unchanged
1720
+ const cp3 = await bucketStorage.getCheckpoint();
1721
+ expect(cp3.lsn).toEqual('3/1');
1722
+ });
1723
+
1724
+ test.runIf(storageVersion >= 3)('deleting while streaming', async () => {
1725
+ await using factory = await generateStorageFactory();
1726
+ const syncRules = await factory.updateSyncRules(
1727
+ updateSyncRulesFromYaml(
1728
+ `
1729
+ bucket_definitions:
1730
+ global:
1731
+ data:
1732
+ - SELECT id, description FROM "%"
1733
+ `,
1734
+ {
1735
+ storageVersion
1736
+ }
1737
+ )
1738
+ );
1739
+ const bucketStorage = factory.getInstance(syncRules);
1740
+
1741
+ const sourceTable = TEST_TABLE;
1742
+ // We simulate two concurrent batches, and nesting is the easiest way to do this.
1743
+ // For this test, we assume that we start with a row "test1", which is picked up by a snapshot
1744
+ // query, right before the delete is streamed. But the snapshot query is only persisted _after_
1745
+ // the delete is streamed, and we need to ensure that the streamed delete takes precedence.
1746
+ await bucketStorage.startBatch({ ...test_utils.BATCH_OPTIONS, skipExistingRows: true }, async (snapshotBatch) => {
1747
+ await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (streamingBatch) => {
1748
+ streamingBatch.save({
1749
+ sourceTable,
1750
+ tag: storage.SaveOperationTag.DELETE,
1751
+ before: {
1752
+ id: 'test1'
1753
+ },
1754
+ beforeReplicaId: test_utils.rid('test1')
1755
+ });
1756
+ await streamingBatch.commit('2/1');
1757
+
1758
+ await snapshotBatch.save({
1759
+ sourceTable,
1760
+ tag: storage.SaveOperationTag.INSERT,
1761
+ after: {
1762
+ id: 'test1',
1763
+ description: 'test1a'
1764
+ },
1765
+ afterReplicaId: test_utils.rid('test1')
1766
+ });
1767
+ await snapshotBatch.markAllSnapshotDone('3/1');
1768
+ await snapshotBatch.commit('1/1');
1769
+
1770
+ await streamingBatch.keepalive('3/1');
1771
+ });
1772
+ });
1773
+
1774
+ const cp = await bucketStorage.getCheckpoint();
1775
+ expect(cp.lsn).toEqual('3/1');
1776
+ const data = await test_utils.fromAsync(
1777
+ bucketStorage.getBucketDataBatch(cp.checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
1778
+ );
1779
+
1780
+ expect(data).toEqual([]);
1781
+ });
1201
1782
  }
1202
1783
 
1203
1784
  /**
@@ -1205,22 +1786,29 @@ bucket_definitions:
1205
1786
  *
1206
1787
  * Exposed as a separate test so we can test with more storage parameters.
1207
1788
  */
1208
- export function testChecksumBatching(generateStorageFactory: storage.TestStorageFactory) {
1789
+ export function testChecksumBatching(config: storage.TestStorageConfig) {
1790
+ const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
1209
1791
  test('checksums for multiple buckets', async () => {
1210
- await using factory = await generateStorageFactory();
1792
+ await using factory = await config.factory();
1211
1793
  const syncRules = await factory.updateSyncRules(
1212
- updateSyncRulesFromYaml(`
1794
+ updateSyncRulesFromYaml(
1795
+ `
1213
1796
  bucket_definitions:
1214
1797
  user:
1215
1798
  parameters: select request.user_id() as user_id
1216
1799
  data:
1217
1800
  - select id, description from test where user_id = bucket.user_id
1218
- `)
1801
+ `,
1802
+ {
1803
+ storageVersion
1804
+ }
1805
+ )
1219
1806
  );
1220
1807
  const bucketStorage = factory.getInstance(syncRules);
1221
1808
 
1222
- const sourceTable = TEST_TABLE;
1809
+ const sourceTable = test_utils.makeTestTable('test', ['id'], config);
1223
1810
  await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1811
+ await batch.markAllSnapshotDone('1/1');
1224
1812
  for (let u of ['u1', 'u2', 'u3', 'u4']) {
1225
1813
  for (let t of ['t1', 't2', 't3', 't4']) {
1226
1814
  const id = `${t}_${u}`;
@@ -1245,10 +1833,10 @@ bucket_definitions:
1245
1833
  const checksums = [...(await bucketStorage.getChecksums(checkpoint, buckets)).values()];
1246
1834
  checksums.sort((a, b) => a.bucket.localeCompare(b.bucket));
1247
1835
  expect(checksums).toEqual([
1248
- { bucket: bucketRequest(syncRules, 'user["u1"]'), count: 4, checksum: 346204588 },
1249
- { bucket: bucketRequest(syncRules, 'user["u2"]'), count: 4, checksum: 5261081 },
1250
- { bucket: bucketRequest(syncRules, 'user["u3"]'), count: 4, checksum: 134760718 },
1251
- { bucket: bucketRequest(syncRules, 'user["u4"]'), count: 4, checksum: -302639724 }
1836
+ { bucket: bucketRequest(syncRules, 'user["u1"]').bucket, count: 4, checksum: 346204588 },
1837
+ { bucket: bucketRequest(syncRules, 'user["u2"]').bucket, count: 4, checksum: 5261081 },
1838
+ { bucket: bucketRequest(syncRules, 'user["u3"]').bucket, count: 4, checksum: 134760718 },
1839
+ { bucket: bucketRequest(syncRules, 'user["u4"]').bucket, count: 4, checksum: -302639724 }
1252
1840
  ]);
1253
1841
  });
1254
1842
  }