@powersync/service-core-tests 0.14.0 → 0.15.1

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 (38) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/test-utils/general-utils.d.ts +22 -3
  3. package/dist/test-utils/general-utils.js +56 -3
  4. package/dist/test-utils/general-utils.js.map +1 -1
  5. package/dist/test-utils/stream_utils.js +2 -2
  6. package/dist/test-utils/stream_utils.js.map +1 -1
  7. package/dist/tests/register-compacting-tests.d.ts +1 -1
  8. package/dist/tests/register-compacting-tests.js +360 -297
  9. package/dist/tests/register-compacting-tests.js.map +1 -1
  10. package/dist/tests/register-data-storage-checkpoint-tests.d.ts +1 -1
  11. package/dist/tests/register-data-storage-checkpoint-tests.js +59 -48
  12. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  13. package/dist/tests/register-data-storage-data-tests.d.ts +2 -2
  14. package/dist/tests/register-data-storage-data-tests.js +1112 -612
  15. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  16. package/dist/tests/register-data-storage-parameter-tests.d.ts +1 -1
  17. package/dist/tests/register-data-storage-parameter-tests.js +273 -254
  18. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  19. package/dist/tests/register-parameter-compacting-tests.d.ts +1 -1
  20. package/dist/tests/register-parameter-compacting-tests.js +83 -87
  21. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  22. package/dist/tests/register-sync-tests.d.ts +2 -1
  23. package/dist/tests/register-sync-tests.js +479 -451
  24. package/dist/tests/register-sync-tests.js.map +1 -1
  25. package/dist/tests/util.d.ts +5 -4
  26. package/dist/tests/util.js +27 -12
  27. package/dist/tests/util.js.map +1 -1
  28. package/package.json +3 -3
  29. package/src/test-utils/general-utils.ts +81 -4
  30. package/src/test-utils/stream_utils.ts +2 -2
  31. package/src/tests/register-compacting-tests.ts +376 -322
  32. package/src/tests/register-data-storage-checkpoint-tests.ts +85 -53
  33. package/src/tests/register-data-storage-data-tests.ts +1050 -559
  34. package/src/tests/register-data-storage-parameter-tests.ts +330 -288
  35. package/src/tests/register-parameter-compacting-tests.ts +87 -90
  36. package/src/tests/register-sync-tests.ts +390 -380
  37. package/src/tests/util.ts +46 -17
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,13 +1,15 @@
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';
11
13
 
12
14
  /**
13
15
  * Normalize data from OplogEntries for comparison in tests.
@@ -30,44 +32,49 @@ const normalizeOplogData = (data: OplogEntry['data']) => {
30
32
  *
31
33
  * ```
32
34
  */
33
- export function registerDataStorageDataTests(generateStorageFactory: storage.TestStorageFactory) {
35
+ export function registerDataStorageDataTests(config: storage.TestStorageConfig) {
36
+ const generateStorageFactory = config.factory;
37
+ const storageVersion = config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
38
+
34
39
  test('removing row', async () => {
35
40
  await using factory = await generateStorageFactory();
36
41
  const syncRules = await factory.updateSyncRules(
37
- updateSyncRulesFromYaml(`
42
+ updateSyncRulesFromYaml(
43
+ `
38
44
  bucket_definitions:
39
45
  global:
40
46
  data:
41
47
  - SELECT id, description FROM "%"
42
- `)
48
+ `,
49
+ { storageVersion }
50
+ )
43
51
  );
44
52
  const bucketStorage = factory.getInstance(syncRules);
53
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
54
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
45
55
 
46
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
47
- const sourceTable = TEST_TABLE;
56
+ await writer.markAllSnapshotDone('1/1');
48
57
 
49
- await batch.save({
50
- sourceTable,
51
- tag: storage.SaveOperationTag.INSERT,
52
- after: {
53
- id: 'test1',
54
- description: 'test1'
55
- },
56
- afterReplicaId: test_utils.rid('test1')
57
- });
58
- await batch.save({
59
- sourceTable,
60
- tag: storage.SaveOperationTag.DELETE,
61
- beforeReplicaId: test_utils.rid('test1')
62
- });
63
- await batch.commit('1/1');
58
+ await writer.save({
59
+ sourceTable: testTable,
60
+ tag: storage.SaveOperationTag.INSERT,
61
+ after: {
62
+ id: 'test1',
63
+ description: 'test1'
64
+ },
65
+ afterReplicaId: test_utils.rid('test1')
64
66
  });
67
+ await writer.save({
68
+ sourceTable: testTable,
69
+ tag: storage.SaveOperationTag.DELETE,
70
+ beforeReplicaId: test_utils.rid('test1')
71
+ });
72
+ await writer.commit('1/1');
65
73
 
66
74
  const { checkpoint } = await bucketStorage.getCheckpoint();
67
75
 
68
- const batch = await test_utils.fromAsync(
69
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
70
- );
76
+ const request = bucketRequest(syncRules, 'global[]');
77
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
71
78
  const data = batch[0].chunkData.data.map((d) => {
72
79
  return {
73
80
  op: d.op,
@@ -84,69 +91,342 @@ bucket_definitions:
84
91
  { op: 'REMOVE', object_id: 'test1', checksum: c2 }
85
92
  ]);
86
93
 
87
- const checksums = [
88
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
89
- ];
94
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
90
95
  expect(checksums).toEqual([
91
96
  {
92
- bucket: bucketRequest(syncRules, 'global[]'),
97
+ bucket: request.bucket,
93
98
  checksum: (c1 + c2) & 0xffffffff,
94
99
  count: 2
95
100
  }
96
101
  ]);
97
102
  });
98
103
 
99
- test('changing client ids', async () => {
104
+ test('insert after delete in new batch', async () => {
100
105
  await using factory = await generateStorageFactory();
101
106
  const syncRules = await factory.updateSyncRules(
102
- updateSyncRulesFromYaml(`
107
+ updateSyncRulesFromYaml(
108
+ `
103
109
  bucket_definitions:
104
110
  global:
105
111
  data:
106
- - SELECT client_id as id, description FROM "%"
107
- `)
112
+ - SELECT id, description FROM "%"
113
+ `,
114
+ { storageVersion }
115
+ )
116
+ );
117
+ const bucketStorage = factory.getInstance(syncRules);
118
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
119
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
120
+ await writer.markAllSnapshotDone('1/1');
121
+
122
+ await writer.save({
123
+ sourceTable: testTable,
124
+ tag: storage.SaveOperationTag.DELETE,
125
+ beforeReplicaId: test_utils.rid('test1')
126
+ });
127
+
128
+ await writer.commit('0/1');
129
+
130
+ await writer.save({
131
+ sourceTable: testTable,
132
+ tag: storage.SaveOperationTag.INSERT,
133
+ after: {
134
+ id: 'test1',
135
+ description: 'test1'
136
+ },
137
+ afterReplicaId: test_utils.rid('test1')
138
+ });
139
+ await writer.commit('2/1');
140
+
141
+ const { checkpoint } = await bucketStorage.getCheckpoint();
142
+
143
+ const request = bucketRequest(syncRules, 'global[]');
144
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
145
+ const data = batch[0].chunkData.data.map((d) => {
146
+ return {
147
+ op: d.op,
148
+ object_id: d.object_id,
149
+ checksum: d.checksum
150
+ };
151
+ });
152
+
153
+ const c1 = 2871785649;
154
+
155
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
156
+
157
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
158
+ expect(checksums).toEqual([
159
+ {
160
+ bucket: request.bucket,
161
+ checksum: c1 & 0xffffffff,
162
+ count: 1
163
+ }
164
+ ]);
165
+ });
166
+
167
+ test('update after delete in new batch', async () => {
168
+ // Update after delete may not be common, but the storage layer should handle it in an eventually-consistent way.
169
+ await using factory = await generateStorageFactory();
170
+ const syncRules = await factory.updateSyncRules(
171
+ updateSyncRulesFromYaml(
172
+ `
173
+ bucket_definitions:
174
+ global:
175
+ data:
176
+ - SELECT id, description FROM "%"
177
+ `,
178
+ { storageVersion }
179
+ )
180
+ );
181
+ const bucketStorage = factory.getInstance(syncRules);
182
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
183
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
184
+ await writer.markAllSnapshotDone('1/1');
185
+
186
+ await writer.save({
187
+ sourceTable: testTable,
188
+ tag: storage.SaveOperationTag.DELETE,
189
+ beforeReplicaId: test_utils.rid('test1')
190
+ });
191
+
192
+ await writer.commit('0/1');
193
+
194
+ await writer.save({
195
+ sourceTable: testTable,
196
+ tag: storage.SaveOperationTag.UPDATE,
197
+ before: {
198
+ id: 'test1'
199
+ },
200
+ after: {
201
+ id: 'test1',
202
+ description: 'test1'
203
+ },
204
+ beforeReplicaId: test_utils.rid('test1'),
205
+ afterReplicaId: test_utils.rid('test1')
206
+ });
207
+ await writer.commit('2/1');
208
+
209
+ const { checkpoint } = await bucketStorage.getCheckpoint();
210
+
211
+ const request = bucketRequest(syncRules, 'global[]');
212
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
213
+ const data = batch[0].chunkData.data.map((d) => {
214
+ return {
215
+ op: d.op,
216
+ object_id: d.object_id,
217
+ checksum: d.checksum
218
+ };
219
+ });
220
+
221
+ const c1 = 2871785649;
222
+
223
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
224
+
225
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
226
+ expect(checksums).toEqual([
227
+ {
228
+ bucket: request.bucket,
229
+ checksum: c1 & 0xffffffff,
230
+ count: 1
231
+ }
232
+ ]);
233
+ });
234
+
235
+ test('insert after delete in same batch', async () => {
236
+ await using factory = await generateStorageFactory();
237
+ const syncRules = await factory.updateSyncRules(
238
+ updateSyncRulesFromYaml(
239
+ `
240
+ bucket_definitions:
241
+ global:
242
+ data:
243
+ - SELECT id, description FROM "%"
244
+ `,
245
+ {
246
+ storageVersion
247
+ }
248
+ )
249
+ );
250
+ const bucketStorage = factory.getInstance(syncRules);
251
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
252
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
253
+ await writer.markAllSnapshotDone('1/1');
254
+
255
+ await writer.save({
256
+ sourceTable: testTable,
257
+ tag: storage.SaveOperationTag.DELETE,
258
+ beforeReplicaId: test_utils.rid('test1')
259
+ });
260
+ await writer.save({
261
+ sourceTable: testTable,
262
+ tag: storage.SaveOperationTag.INSERT,
263
+ after: {
264
+ id: 'test1',
265
+ description: 'test1'
266
+ },
267
+ afterReplicaId: test_utils.rid('test1')
268
+ });
269
+ await writer.commit('1/1');
270
+
271
+ const { checkpoint } = await bucketStorage.getCheckpoint();
272
+
273
+ const request = bucketRequest(syncRules, 'global[]');
274
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
275
+ const data = batch[0].chunkData.data.map((d) => {
276
+ return {
277
+ op: d.op,
278
+ object_id: d.object_id,
279
+ checksum: d.checksum
280
+ };
281
+ });
282
+
283
+ const c1 = 2871785649;
284
+
285
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
286
+
287
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
288
+ expect(checksums).toEqual([
289
+ {
290
+ bucket: request.bucket,
291
+ checksum: c1 & 0xffffffff,
292
+ count: 1
293
+ }
294
+ ]);
295
+ });
296
+
297
+ test('(insert, delete, insert), (delete)', async () => {
298
+ await using factory = await generateStorageFactory();
299
+ const syncRules = await factory.updateSyncRules(
300
+ updateSyncRulesFromYaml(
301
+ `
302
+ bucket_definitions:
303
+ global:
304
+ data:
305
+ - SELECT id, description FROM "%"
306
+ `,
307
+ {
308
+ storageVersion
309
+ }
310
+ )
108
311
  );
109
312
  const bucketStorage = factory.getInstance(syncRules);
313
+ {
314
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
315
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
316
+ await writer.markAllSnapshotDone('1/1');
110
317
 
111
- const sourceTable = TEST_TABLE;
112
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
113
- await batch.save({
318
+ await writer.save({
114
319
  sourceTable,
115
320
  tag: storage.SaveOperationTag.INSERT,
116
321
  after: {
117
322
  id: 'test1',
118
- client_id: 'client1a',
119
- description: 'test1a'
323
+ description: 'test1'
120
324
  },
121
325
  afterReplicaId: test_utils.rid('test1')
122
326
  });
123
- await batch.save({
327
+ await writer.save({
328
+ sourceTable,
329
+ tag: storage.SaveOperationTag.DELETE,
330
+ beforeReplicaId: test_utils.rid('test1')
331
+ });
332
+ await writer.save({
124
333
  sourceTable,
125
- tag: storage.SaveOperationTag.UPDATE,
334
+ tag: storage.SaveOperationTag.INSERT,
126
335
  after: {
127
336
  id: 'test1',
128
- client_id: 'client1b',
129
- description: 'test1b'
337
+ description: 'test1'
130
338
  },
131
339
  afterReplicaId: test_utils.rid('test1')
132
340
  });
341
+ await writer.commit('1/1');
342
+ }
133
343
 
134
- await batch.save({
344
+ {
345
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
346
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
347
+ await writer.markAllSnapshotDone('1/1');
348
+
349
+ await writer.save({
135
350
  sourceTable,
136
- tag: storage.SaveOperationTag.INSERT,
137
- after: {
138
- id: 'test2',
139
- client_id: 'client2',
140
- description: 'test2'
141
- },
142
- afterReplicaId: test_utils.rid('test2')
351
+ tag: storage.SaveOperationTag.DELETE,
352
+ beforeReplicaId: test_utils.rid('test1')
143
353
  });
354
+ await writer.commit('2/1');
355
+ }
356
+
357
+ const { checkpoint } = await bucketStorage.getCheckpoint();
358
+
359
+ const request = bucketRequest(syncRules, 'global[]');
360
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
361
+
362
+ expect(reduceBucket(batch[0].chunkData.data).slice(1)).toEqual([]);
363
+
364
+ const data = batch[0].chunkData.data.map((d) => {
365
+ return {
366
+ op: d.op,
367
+ object_id: d.object_id,
368
+ checksum: d.checksum
369
+ };
370
+ });
371
+
372
+ expect(data).toMatchSnapshot();
373
+ });
374
+
375
+ test('changing client ids', async () => {
376
+ await using factory = await generateStorageFactory();
377
+ const syncRules = await factory.updateSyncRules(
378
+ updateSyncRulesFromYaml(
379
+ `
380
+ bucket_definitions:
381
+ global:
382
+ data:
383
+ - SELECT client_id as id, description FROM "%"
384
+ `,
385
+ {
386
+ storageVersion
387
+ }
388
+ )
389
+ );
390
+ const bucketStorage = factory.getInstance(syncRules);
391
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
392
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
393
+ await writer.markAllSnapshotDone('1/1');
394
+ await writer.save({
395
+ sourceTable,
396
+ tag: storage.SaveOperationTag.INSERT,
397
+ after: {
398
+ id: 'test1',
399
+ client_id: 'client1a',
400
+ description: 'test1a'
401
+ },
402
+ afterReplicaId: test_utils.rid('test1')
403
+ });
404
+ await writer.save({
405
+ sourceTable,
406
+ tag: storage.SaveOperationTag.UPDATE,
407
+ after: {
408
+ id: 'test1',
409
+ client_id: 'client1b',
410
+ description: 'test1b'
411
+ },
412
+ afterReplicaId: test_utils.rid('test1')
413
+ });
144
414
 
145
- await batch.commit('1/1');
415
+ await writer.save({
416
+ sourceTable,
417
+ tag: storage.SaveOperationTag.INSERT,
418
+ after: {
419
+ id: 'test2',
420
+ client_id: 'client2',
421
+ description: 'test2'
422
+ },
423
+ afterReplicaId: test_utils.rid('test2')
146
424
  });
425
+
426
+ await writer.commit('1/1');
147
427
  const { checkpoint } = await bucketStorage.getCheckpoint();
148
428
  const batch = await test_utils.fromAsync(
149
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
429
+ bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRules, 'global[]')])
150
430
  );
151
431
  const data = batch[0].chunkData.data.map((d) => {
152
432
  return {
@@ -166,56 +446,53 @@ bucket_definitions:
166
446
  test('re-apply delete', async () => {
167
447
  await using factory = await generateStorageFactory();
168
448
  const syncRules = await factory.updateSyncRules(
169
- updateSyncRulesFromYaml(`
449
+ updateSyncRulesFromYaml(
450
+ `
170
451
  bucket_definitions:
171
452
  global:
172
453
  data:
173
454
  - SELECT id, description FROM "%"
174
- `)
455
+ `,
456
+ {
457
+ storageVersion
458
+ }
459
+ )
175
460
  );
176
461
  const bucketStorage = factory.getInstance(syncRules);
177
-
178
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
179
- const sourceTable = TEST_TABLE;
180
-
181
- await batch.save({
182
- sourceTable,
183
- tag: storage.SaveOperationTag.INSERT,
184
- after: {
185
- id: 'test1',
186
- description: 'test1'
187
- },
188
- afterReplicaId: test_utils.rid('test1')
189
- });
462
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
463
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
464
+ await writer.markAllSnapshotDone('1/1');
465
+
466
+ await writer.save({
467
+ sourceTable,
468
+ tag: storage.SaveOperationTag.INSERT,
469
+ after: {
470
+ id: 'test1',
471
+ description: 'test1'
472
+ },
473
+ afterReplicaId: test_utils.rid('test1')
190
474
  });
475
+ await writer.flush();
191
476
 
192
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
193
- const sourceTable = TEST_TABLE;
194
-
195
- await batch.save({
196
- sourceTable,
197
- tag: storage.SaveOperationTag.DELETE,
198
- beforeReplicaId: test_utils.rid('test1')
199
- });
200
-
201
- await batch.commit('1/1');
477
+ await writer.save({
478
+ sourceTable,
479
+ tag: storage.SaveOperationTag.DELETE,
480
+ beforeReplicaId: test_utils.rid('test1')
202
481
  });
203
482
 
204
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
205
- const sourceTable = TEST_TABLE;
483
+ await writer.commit('1/1');
206
484
 
207
- await batch.save({
208
- sourceTable,
209
- tag: storage.SaveOperationTag.DELETE,
210
- beforeReplicaId: test_utils.rid('test1')
211
- });
485
+ await writer.save({
486
+ sourceTable,
487
+ tag: storage.SaveOperationTag.DELETE,
488
+ beforeReplicaId: test_utils.rid('test1')
212
489
  });
490
+ await writer.flush();
213
491
 
214
492
  const { checkpoint } = await bucketStorage.getCheckpoint();
215
493
 
216
- const batch = await test_utils.fromAsync(
217
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
218
- );
494
+ const request = bucketRequest(syncRules, 'global[]');
495
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
219
496
  const data = batch[0].chunkData.data.map((d) => {
220
497
  return {
221
498
  op: d.op,
@@ -232,12 +509,10 @@ bucket_definitions:
232
509
  { op: 'REMOVE', object_id: 'test1', checksum: c2 }
233
510
  ]);
234
511
 
235
- const checksums = [
236
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
237
- ];
512
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
238
513
  expect(checksums).toEqual([
239
514
  {
240
- bucket: bucketRequest(syncRules, 'global[]'),
515
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
241
516
  checksum: (c1 + c2) & 0xffffffff,
242
517
  count: 2
243
518
  }
@@ -247,98 +522,96 @@ bucket_definitions:
247
522
  test('re-apply update + delete', async () => {
248
523
  await using factory = await generateStorageFactory();
249
524
  const syncRules = await factory.updateSyncRules(
250
- updateSyncRulesFromYaml(`
525
+ updateSyncRulesFromYaml(
526
+ `
251
527
  bucket_definitions:
252
528
  global:
253
529
  data:
254
530
  - SELECT id, description FROM "%"
255
- `)
531
+ `,
532
+ { storageVersion }
533
+ )
256
534
  );
257
535
  const bucketStorage = factory.getInstance(syncRules);
258
-
259
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
260
- const sourceTable = TEST_TABLE;
261
-
262
- await batch.save({
263
- sourceTable,
264
- tag: storage.SaveOperationTag.INSERT,
265
- after: {
266
- id: 'test1',
267
- description: 'test1'
268
- },
269
- afterReplicaId: test_utils.rid('test1')
270
- });
536
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
537
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
538
+ await writer.markAllSnapshotDone('1/1');
539
+
540
+ await writer.save({
541
+ sourceTable,
542
+ tag: storage.SaveOperationTag.INSERT,
543
+ after: {
544
+ id: 'test1',
545
+ description: 'test1'
546
+ },
547
+ afterReplicaId: test_utils.rid('test1')
271
548
  });
549
+ await writer.flush();
272
550
 
273
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
274
- const sourceTable = TEST_TABLE;
275
-
276
- await batch.save({
277
- sourceTable,
278
- tag: storage.SaveOperationTag.UPDATE,
279
- after: {
280
- id: 'test1',
281
- description: undefined
282
- },
283
- afterReplicaId: test_utils.rid('test1')
284
- });
551
+ await writer.markAllSnapshotDone('1/1');
285
552
 
286
- await batch.save({
287
- sourceTable,
288
- tag: storage.SaveOperationTag.UPDATE,
289
- after: {
290
- id: 'test1',
291
- description: undefined
292
- },
293
- afterReplicaId: test_utils.rid('test1')
294
- });
553
+ await writer.save({
554
+ sourceTable,
555
+ tag: storage.SaveOperationTag.UPDATE,
556
+ after: {
557
+ id: 'test1',
558
+ description: undefined
559
+ },
560
+ afterReplicaId: test_utils.rid('test1')
561
+ });
295
562
 
296
- await batch.save({
297
- sourceTable,
298
- tag: storage.SaveOperationTag.DELETE,
299
- beforeReplicaId: test_utils.rid('test1')
300
- });
563
+ await writer.save({
564
+ sourceTable,
565
+ tag: storage.SaveOperationTag.UPDATE,
566
+ after: {
567
+ id: 'test1',
568
+ description: undefined
569
+ },
570
+ afterReplicaId: test_utils.rid('test1')
571
+ });
301
572
 
302
- await batch.commit('1/1');
573
+ await writer.save({
574
+ sourceTable,
575
+ tag: storage.SaveOperationTag.DELETE,
576
+ beforeReplicaId: test_utils.rid('test1')
303
577
  });
304
578
 
305
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
306
- const sourceTable = TEST_TABLE;
579
+ await writer.commit('1/1');
307
580
 
308
- await batch.save({
309
- sourceTable,
310
- tag: storage.SaveOperationTag.UPDATE,
311
- after: {
312
- id: 'test1',
313
- description: undefined
314
- },
315
- afterReplicaId: test_utils.rid('test1')
316
- });
581
+ await writer.markAllSnapshotDone('1/1');
317
582
 
318
- await batch.save({
319
- sourceTable,
320
- tag: storage.SaveOperationTag.UPDATE,
321
- after: {
322
- id: 'test1',
323
- description: undefined
324
- },
325
- afterReplicaId: test_utils.rid('test1')
326
- });
583
+ await writer.save({
584
+ sourceTable,
585
+ tag: storage.SaveOperationTag.UPDATE,
586
+ after: {
587
+ id: 'test1',
588
+ description: undefined
589
+ },
590
+ afterReplicaId: test_utils.rid('test1')
591
+ });
327
592
 
328
- await batch.save({
329
- sourceTable,
330
- tag: storage.SaveOperationTag.DELETE,
331
- beforeReplicaId: test_utils.rid('test1')
332
- });
593
+ await writer.save({
594
+ sourceTable,
595
+ tag: storage.SaveOperationTag.UPDATE,
596
+ after: {
597
+ id: 'test1',
598
+ description: undefined
599
+ },
600
+ afterReplicaId: test_utils.rid('test1')
601
+ });
333
602
 
334
- await batch.commit('2/1');
603
+ await writer.save({
604
+ sourceTable,
605
+ tag: storage.SaveOperationTag.DELETE,
606
+ beforeReplicaId: test_utils.rid('test1')
335
607
  });
336
608
 
609
+ await writer.commit('2/1');
610
+
337
611
  const { checkpoint } = await bucketStorage.getCheckpoint();
338
612
 
339
- const batch = await test_utils.fromAsync(
340
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
341
- );
613
+ const request = bucketRequest(syncRules, 'global[]');
614
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
342
615
 
343
616
  const data = batch[0].chunkData.data.map((d) => {
344
617
  return {
@@ -358,12 +631,10 @@ bucket_definitions:
358
631
  { op: 'REMOVE', object_id: 'test1', checksum: c2 }
359
632
  ]);
360
633
 
361
- const checksums = [
362
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
363
- ];
634
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
364
635
  expect(checksums).toEqual([
365
636
  {
366
- bucket: bucketRequest(syncRules, 'global[]'),
637
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
367
638
  checksum: (c1 + c1 + c1 + c2) & 0xffffffff,
368
639
  count: 4
369
640
  }
@@ -381,127 +652,128 @@ bucket_definitions:
381
652
 
382
653
  await using factory = await generateStorageFactory();
383
654
  const syncRules = await factory.updateSyncRules(
384
- updateSyncRulesFromYaml(`
655
+ updateSyncRulesFromYaml(
656
+ `
385
657
  bucket_definitions:
386
658
  global:
387
659
  data:
388
660
  - SELECT id, description FROM "test"
389
- `)
661
+ `,
662
+ { storageVersion }
663
+ )
390
664
  );
391
665
  const bucketStorage = factory.getInstance(syncRules);
666
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
667
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
392
668
 
393
669
  // Pre-setup
394
- const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
395
- const sourceTable = TEST_TABLE;
396
-
397
- await batch.save({
398
- sourceTable,
399
- tag: storage.SaveOperationTag.INSERT,
400
- after: {
401
- id: 'test1',
402
- description: 'test1a'
403
- },
404
- afterReplicaId: test_utils.rid('test1')
405
- });
670
+ await writer.markAllSnapshotDone('1/1');
671
+
672
+ await writer.save({
673
+ sourceTable,
674
+ tag: storage.SaveOperationTag.INSERT,
675
+ after: {
676
+ id: 'test1',
677
+ description: 'test1a'
678
+ },
679
+ afterReplicaId: test_utils.rid('test1')
680
+ });
406
681
 
407
- await batch.save({
408
- sourceTable,
409
- tag: storage.SaveOperationTag.INSERT,
410
- after: {
411
- id: 'test2',
412
- description: 'test2a'
413
- },
414
- afterReplicaId: test_utils.rid('test2')
415
- });
682
+ await writer.save({
683
+ sourceTable,
684
+ tag: storage.SaveOperationTag.INSERT,
685
+ after: {
686
+ id: 'test2',
687
+ description: 'test2a'
688
+ },
689
+ afterReplicaId: test_utils.rid('test2')
416
690
  });
691
+ const result1 = await writer.flush();
417
692
 
418
693
  const checkpoint1 = result1?.flushed_op ?? 0n;
419
694
 
420
695
  // Test batch
421
- const result2 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
422
- const sourceTable = TEST_TABLE;
423
- // b
424
- await batch.save({
425
- sourceTable,
426
- tag: storage.SaveOperationTag.INSERT,
427
- after: {
428
- id: 'test1',
429
- description: 'test1b'
430
- },
431
- afterReplicaId: test_utils.rid('test1')
432
- });
433
-
434
- await batch.save({
435
- sourceTable,
436
- tag: storage.SaveOperationTag.UPDATE,
437
- before: {
438
- id: 'test1'
439
- },
440
- beforeReplicaId: test_utils.rid('test1'),
441
- after: {
442
- id: 'test2',
443
- description: 'test2b'
444
- },
445
- afterReplicaId: test_utils.rid('test2')
446
- });
447
-
448
- await batch.save({
449
- sourceTable,
450
- tag: storage.SaveOperationTag.UPDATE,
451
- before: {
452
- id: 'test2'
453
- },
454
- beforeReplicaId: test_utils.rid('test2'),
455
- after: {
456
- id: 'test3',
457
- description: 'test3b'
458
- },
696
+ // b
697
+ await writer.save({
698
+ sourceTable,
699
+ tag: storage.SaveOperationTag.INSERT,
700
+ after: {
701
+ id: 'test1',
702
+ description: 'test1b'
703
+ },
704
+ afterReplicaId: test_utils.rid('test1')
705
+ });
459
706
 
460
- afterReplicaId: test_utils.rid('test3')
461
- });
707
+ await writer.save({
708
+ sourceTable,
709
+ tag: storage.SaveOperationTag.UPDATE,
710
+ before: {
711
+ id: 'test1'
712
+ },
713
+ beforeReplicaId: test_utils.rid('test1'),
714
+ after: {
715
+ id: 'test2',
716
+ description: 'test2b'
717
+ },
718
+ afterReplicaId: test_utils.rid('test2')
719
+ });
462
720
 
463
- // c
464
- await batch.save({
465
- sourceTable,
466
- tag: storage.SaveOperationTag.UPDATE,
467
- after: {
468
- id: 'test2',
469
- description: 'test2c'
470
- },
471
- afterReplicaId: test_utils.rid('test2')
472
- });
721
+ await writer.save({
722
+ sourceTable,
723
+ tag: storage.SaveOperationTag.UPDATE,
724
+ before: {
725
+ id: 'test2'
726
+ },
727
+ beforeReplicaId: test_utils.rid('test2'),
728
+ after: {
729
+ id: 'test3',
730
+ description: 'test3b'
731
+ },
473
732
 
474
- // d
475
- await batch.save({
476
- sourceTable,
477
- tag: storage.SaveOperationTag.INSERT,
478
- after: {
479
- id: 'test4',
480
- description: 'test4d'
481
- },
482
- afterReplicaId: test_utils.rid('test4')
483
- });
733
+ afterReplicaId: test_utils.rid('test3')
734
+ });
484
735
 
485
- await batch.save({
486
- sourceTable,
487
- tag: storage.SaveOperationTag.UPDATE,
488
- before: {
489
- id: 'test4'
490
- },
491
- beforeReplicaId: test_utils.rid('test4'),
492
- after: {
493
- id: 'test5',
494
- description: 'test5d'
495
- },
496
- afterReplicaId: test_utils.rid('test5')
497
- });
736
+ // c
737
+ await writer.save({
738
+ sourceTable,
739
+ tag: storage.SaveOperationTag.UPDATE,
740
+ after: {
741
+ id: 'test2',
742
+ description: 'test2c'
743
+ },
744
+ afterReplicaId: test_utils.rid('test2')
745
+ });
746
+
747
+ // d
748
+ await writer.save({
749
+ sourceTable,
750
+ tag: storage.SaveOperationTag.INSERT,
751
+ after: {
752
+ id: 'test4',
753
+ description: 'test4d'
754
+ },
755
+ afterReplicaId: test_utils.rid('test4')
756
+ });
757
+
758
+ await writer.save({
759
+ sourceTable,
760
+ tag: storage.SaveOperationTag.UPDATE,
761
+ before: {
762
+ id: 'test4'
763
+ },
764
+ beforeReplicaId: test_utils.rid('test4'),
765
+ after: {
766
+ id: 'test5',
767
+ description: 'test5d'
768
+ },
769
+ afterReplicaId: test_utils.rid('test5')
498
770
  });
771
+ const result2 = await writer.flush();
499
772
 
500
773
  const checkpoint2 = result2!.flushed_op;
501
774
 
502
- const batch = await test_utils.fromAsync(
503
- bucketStorage.getBucketDataBatch(checkpoint2, bucketRequestMap(syncRules, [['global[]', checkpoint1]]))
504
- );
775
+ const request = bucketRequest(syncRules, 'global[]', checkpoint1);
776
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint2, [request]));
505
777
 
506
778
  const data = batch[0].chunkData.data.map((d) => {
507
779
  return {
@@ -539,68 +811,73 @@ bucket_definitions:
539
811
  }
540
812
  await using factory = await generateStorageFactory();
541
813
  const syncRules = await factory.updateSyncRules(
542
- updateSyncRulesFromYaml(`
814
+ updateSyncRulesFromYaml(
815
+ `
543
816
  bucket_definitions:
544
817
  global:
545
818
  data:
546
819
  - SELECT id, description FROM "test"
547
- `)
820
+ `,
821
+ {
822
+ storageVersion
823
+ }
824
+ )
548
825
  );
549
826
  const bucketStorage = factory.getInstance(syncRules);
550
827
 
551
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
828
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
829
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
552
830
 
553
831
  // Pre-setup
554
- const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
555
- await batch.save({
556
- sourceTable,
557
- tag: storage.SaveOperationTag.INSERT,
558
- after: {
559
- id: 'test1',
560
- description: 'test1a'
561
- },
562
- afterReplicaId: rid2('test1', 'test1a')
563
- });
832
+ await writer.markAllSnapshotDone('1/1');
833
+ await writer.save({
834
+ sourceTable,
835
+ tag: storage.SaveOperationTag.INSERT,
836
+ after: {
837
+ id: 'test1',
838
+ description: 'test1a'
839
+ },
840
+ afterReplicaId: rid2('test1', 'test1a')
564
841
  });
842
+ const result1 = await writer.flush();
565
843
 
566
844
  const checkpoint1 = result1?.flushed_op ?? 0n;
567
845
 
568
- const result2 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
569
- // Unchanged, but has a before id
570
- await batch.save({
571
- sourceTable,
572
- tag: storage.SaveOperationTag.UPDATE,
573
- before: {
574
- id: 'test1',
575
- description: 'test1a'
576
- },
577
- beforeReplicaId: rid2('test1', 'test1a'),
578
- after: {
579
- id: 'test1',
580
- description: 'test1b'
581
- },
582
- afterReplicaId: rid2('test1', 'test1b')
583
- });
846
+ // Unchanged, but has a before id
847
+ await writer.save({
848
+ sourceTable,
849
+ tag: storage.SaveOperationTag.UPDATE,
850
+ before: {
851
+ id: 'test1',
852
+ description: 'test1a'
853
+ },
854
+ beforeReplicaId: rid2('test1', 'test1a'),
855
+ after: {
856
+ id: 'test1',
857
+ description: 'test1b'
858
+ },
859
+ afterReplicaId: rid2('test1', 'test1b')
584
860
  });
585
-
586
- const result3 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
587
- // Delete
588
- await batch.save({
589
- sourceTable,
590
- tag: storage.SaveOperationTag.DELETE,
591
- before: {
592
- id: 'test1',
593
- description: 'test1b'
594
- },
595
- beforeReplicaId: rid2('test1', 'test1b'),
596
- after: undefined
597
- });
861
+ const result2 = await writer.flush();
862
+
863
+ // Delete
864
+ await writer.save({
865
+ sourceTable,
866
+ tag: storage.SaveOperationTag.DELETE,
867
+ before: {
868
+ id: 'test1',
869
+ description: 'test1b'
870
+ },
871
+ beforeReplicaId: rid2('test1', 'test1b'),
872
+ after: undefined
598
873
  });
874
+ const result3 = await writer.flush();
599
875
 
600
876
  const checkpoint3 = result3!.flushed_op;
601
877
 
878
+ const request = bucketRequest(syncRules, 'global[]');
602
879
  const batch = await test_utils.fromAsync(
603
- bucketStorage.getBucketDataBatch(checkpoint3, bucketRequestMap(syncRules, [['global[]', checkpoint1]]))
880
+ bucketStorage.getBucketDataBatch(checkpoint3, [{ ...request, start: checkpoint1 }])
604
881
  );
605
882
  const data = batch[0].chunkData.data.map((d) => {
606
883
  return {
@@ -647,68 +924,73 @@ bucket_definitions:
647
924
 
648
925
  await using factory = await generateStorageFactory();
649
926
  const syncRules = await factory.updateSyncRules(
650
- updateSyncRulesFromYaml(`
927
+ updateSyncRulesFromYaml(
928
+ `
651
929
  bucket_definitions:
652
930
  global:
653
931
  data:
654
932
  - SELECT id, description FROM "test"
655
- `)
933
+ `,
934
+ {
935
+ storageVersion
936
+ }
937
+ )
656
938
  );
657
939
  const bucketStorage = factory.getInstance(syncRules);
658
940
 
659
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
941
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
942
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
660
943
 
661
944
  // Pre-setup
662
- const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
663
- await batch.save({
664
- sourceTable,
665
- tag: storage.SaveOperationTag.INSERT,
666
- after: {
667
- id: 'test1',
668
- description: 'test1a'
669
- },
670
- afterReplicaId: rid2('test1', 'test1a')
671
- });
945
+ await writer.markAllSnapshotDone('1/1');
946
+ await writer.save({
947
+ sourceTable,
948
+ tag: storage.SaveOperationTag.INSERT,
949
+ after: {
950
+ id: 'test1',
951
+ description: 'test1a'
952
+ },
953
+ afterReplicaId: rid2('test1', 'test1a')
672
954
  });
955
+ const result1 = await writer.flush();
673
956
 
674
957
  const checkpoint1 = result1?.flushed_op ?? 0n;
675
958
 
676
- const result2 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
677
- // Unchanged, but has a before id
678
- await batch.save({
679
- sourceTable,
680
- tag: storage.SaveOperationTag.UPDATE,
681
- before: {
682
- id: 'test1',
683
- description: 'test1a'
684
- },
685
- beforeReplicaId: rid2('test1', 'test1a'),
686
- after: {
687
- id: 'test1',
688
- description: 'test1a'
689
- },
690
- afterReplicaId: rid2('test1', 'test1a')
691
- });
959
+ // Unchanged, but has a before id
960
+ await writer.save({
961
+ sourceTable,
962
+ tag: storage.SaveOperationTag.UPDATE,
963
+ before: {
964
+ id: 'test1',
965
+ description: 'test1a'
966
+ },
967
+ beforeReplicaId: rid2('test1', 'test1a'),
968
+ after: {
969
+ id: 'test1',
970
+ description: 'test1a'
971
+ },
972
+ afterReplicaId: rid2('test1', 'test1a')
692
973
  });
693
-
694
- const result3 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
695
- // Delete
696
- await batch.save({
697
- sourceTable,
698
- tag: storage.SaveOperationTag.DELETE,
699
- before: {
700
- id: 'test1',
701
- description: 'test1a'
702
- },
703
- beforeReplicaId: rid2('test1', 'test1a'),
704
- after: undefined
705
- });
974
+ const result2 = await writer.flush();
975
+
976
+ // Delete
977
+ await writer.save({
978
+ sourceTable,
979
+ tag: storage.SaveOperationTag.DELETE,
980
+ before: {
981
+ id: 'test1',
982
+ description: 'test1a'
983
+ },
984
+ beforeReplicaId: rid2('test1', 'test1a'),
985
+ after: undefined
706
986
  });
987
+ const result3 = await writer.flush();
707
988
 
708
989
  const checkpoint3 = result3!.flushed_op;
709
990
 
991
+ const request = bucketRequest(syncRules, 'global[]');
710
992
  const batch = await test_utils.fromAsync(
711
- bucketStorage.getBucketDataBatch(checkpoint3, bucketRequestMap(syncRules, [['global[]', checkpoint1]]))
993
+ bucketStorage.getBucketDataBatch(checkpoint3, [{ ...request, start: checkpoint1 }])
712
994
  );
713
995
  const data = batch[0].chunkData.data.map((d) => {
714
996
  return {
@@ -745,73 +1027,76 @@ bucket_definitions:
745
1027
  // and the test will have to updated when other implementations are added.
746
1028
  await using factory = await generateStorageFactory();
747
1029
  const syncRules = await factory.updateSyncRules(
748
- updateSyncRulesFromYaml(`
1030
+ updateSyncRulesFromYaml(
1031
+ `
749
1032
  bucket_definitions:
750
1033
  global:
751
1034
  data:
752
1035
  - SELECT id, description FROM "%"
753
- `)
1036
+ `,
1037
+ {
1038
+ storageVersion
1039
+ }
1040
+ )
754
1041
  );
755
1042
  const bucketStorage = factory.getInstance(syncRules);
1043
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1044
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1045
+ await writer.markAllSnapshotDone('1/1');
1046
+
1047
+ const largeDescription = '0123456789'.repeat(12_000_00);
1048
+
1049
+ await writer.save({
1050
+ sourceTable,
1051
+ tag: storage.SaveOperationTag.INSERT,
1052
+ after: {
1053
+ id: 'test1',
1054
+ description: 'test1'
1055
+ },
1056
+ afterReplicaId: test_utils.rid('test1')
1057
+ });
756
1058
 
757
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
758
- const sourceTable = TEST_TABLE;
759
-
760
- const largeDescription = '0123456789'.repeat(12_000_00);
761
-
762
- await batch.save({
763
- sourceTable,
764
- tag: storage.SaveOperationTag.INSERT,
765
- after: {
766
- id: 'test1',
767
- description: 'test1'
768
- },
769
- afterReplicaId: test_utils.rid('test1')
770
- });
771
-
772
- await batch.save({
773
- sourceTable,
774
- tag: storage.SaveOperationTag.INSERT,
775
- after: {
776
- id: 'large1',
777
- description: largeDescription
778
- },
779
- afterReplicaId: test_utils.rid('large1')
780
- });
781
-
782
- // Large enough to split the returned batch
783
- await batch.save({
784
- sourceTable,
785
- tag: storage.SaveOperationTag.INSERT,
786
- after: {
787
- id: 'large2',
788
- description: largeDescription
789
- },
790
- afterReplicaId: test_utils.rid('large2')
791
- });
1059
+ await writer.save({
1060
+ sourceTable,
1061
+ tag: storage.SaveOperationTag.INSERT,
1062
+ after: {
1063
+ id: 'large1',
1064
+ description: largeDescription
1065
+ },
1066
+ afterReplicaId: test_utils.rid('large1')
1067
+ });
792
1068
 
793
- await batch.save({
794
- sourceTable,
795
- tag: storage.SaveOperationTag.INSERT,
796
- after: {
797
- id: 'test3',
798
- description: 'test3'
799
- },
800
- afterReplicaId: test_utils.rid('test3')
801
- });
1069
+ // Large enough to split the returned batch
1070
+ await writer.save({
1071
+ sourceTable,
1072
+ tag: storage.SaveOperationTag.INSERT,
1073
+ after: {
1074
+ id: 'large2',
1075
+ description: largeDescription
1076
+ },
1077
+ afterReplicaId: test_utils.rid('large2')
1078
+ });
802
1079
 
803
- await batch.commit('1/1');
1080
+ await writer.save({
1081
+ sourceTable,
1082
+ tag: storage.SaveOperationTag.INSERT,
1083
+ after: {
1084
+ id: 'test3',
1085
+ description: 'test3'
1086
+ },
1087
+ afterReplicaId: test_utils.rid('test3')
804
1088
  });
805
1089
 
1090
+ await writer.commit('1/1');
1091
+
806
1092
  const { checkpoint } = await bucketStorage.getCheckpoint();
807
1093
 
808
1094
  const options: storage.BucketDataBatchOptions = {
809
1095
  chunkLimitBytes: 16 * 1024 * 1024
810
1096
  };
811
1097
 
812
- const batch1 = await test_utils.fromAsync(
813
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]), options)
814
- );
1098
+ const request = bucketRequest(syncRules, 'global[]');
1099
+ const batch1 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request], options));
815
1100
  expect(test_utils.getBatchData(batch1)).toEqual([
816
1101
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
817
1102
  { op_id: '2', op: 'PUT', object_id: 'large1', checksum: 454746904 }
@@ -825,7 +1110,7 @@ bucket_definitions:
825
1110
  const batch2 = await test_utils.fromAsync(
826
1111
  bucketStorage.getBucketDataBatch(
827
1112
  checkpoint,
828
- bucketRequestMap(syncRules, [['global[]', BigInt(batch1[0].chunkData.next_after)]]),
1113
+ [{ ...request, start: BigInt(batch1[0].chunkData.next_after) }],
829
1114
  options
830
1115
  )
831
1116
  );
@@ -842,7 +1127,7 @@ bucket_definitions:
842
1127
  const batch3 = await test_utils.fromAsync(
843
1128
  bucketStorage.getBucketDataBatch(
844
1129
  checkpoint,
845
- bucketRequestMap(syncRules, [['global[]', BigInt(batch2[0].chunkData.next_after)]]),
1130
+ [{ ...request, start: BigInt(batch2[0].chunkData.next_after) }],
846
1131
  options
847
1132
  )
848
1133
  );
@@ -854,38 +1139,41 @@ bucket_definitions:
854
1139
  // Test syncing a batch of data that is limited by count.
855
1140
  await using factory = await generateStorageFactory();
856
1141
  const syncRules = await factory.updateSyncRules(
857
- updateSyncRulesFromYaml(`
1142
+ updateSyncRulesFromYaml(
1143
+ `
858
1144
  bucket_definitions:
859
1145
  global:
860
1146
  data:
861
1147
  - SELECT id, description FROM "%"
862
- `)
1148
+ `,
1149
+ {
1150
+ storageVersion
1151
+ }
1152
+ )
863
1153
  );
864
1154
  const bucketStorage = factory.getInstance(syncRules);
1155
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1156
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1157
+ await writer.markAllSnapshotDone('1/1');
865
1158
 
866
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
867
- const sourceTable = TEST_TABLE;
868
-
869
- for (let i = 1; i <= 6; i++) {
870
- await batch.save({
871
- sourceTable,
872
- tag: storage.SaveOperationTag.INSERT,
873
- after: {
874
- id: `test${i}`,
875
- description: `test${i}`
876
- },
877
- afterReplicaId: `test${i}`
878
- });
879
- }
1159
+ for (let i = 1; i <= 6; i++) {
1160
+ await writer.save({
1161
+ sourceTable,
1162
+ tag: storage.SaveOperationTag.INSERT,
1163
+ after: {
1164
+ id: `test${i}`,
1165
+ description: `test${i}`
1166
+ },
1167
+ afterReplicaId: `test${i}`
1168
+ });
1169
+ }
880
1170
 
881
- await batch.commit('1/1');
882
- });
1171
+ await writer.commit('1/1');
883
1172
 
884
1173
  const { checkpoint } = await bucketStorage.getCheckpoint();
885
1174
 
886
- const batch1 = await test_utils.oneFromAsync(
887
- bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]), { limit: 4 })
888
- );
1175
+ const request = bucketRequest(syncRules, 'global[]');
1176
+ const batch1 = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request], { limit: 4 }));
889
1177
 
890
1178
  expect(test_utils.getBatchData(batch1)).toEqual([
891
1179
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
@@ -901,13 +1189,9 @@ bucket_definitions:
901
1189
  });
902
1190
 
903
1191
  const batch2 = await test_utils.oneFromAsync(
904
- bucketStorage.getBucketDataBatch(
905
- checkpoint,
906
- bucketRequestMap(syncRules, [['global[]', BigInt(batch1.chunkData.next_after)]]),
907
- {
908
- limit: 4
909
- }
910
- )
1192
+ bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch1.chunkData.next_after) }], {
1193
+ limit: 4
1194
+ })
911
1195
  );
912
1196
  expect(test_utils.getBatchData(batch2)).toEqual([
913
1197
  { op_id: '5', op: 'PUT', object_id: 'test5', checksum: 3686902721 },
@@ -921,13 +1205,9 @@ bucket_definitions:
921
1205
  });
922
1206
 
923
1207
  const batch3 = await test_utils.fromAsync(
924
- bucketStorage.getBucketDataBatch(
925
- checkpoint,
926
- bucketRequestMap(syncRules, [['global[]', BigInt(batch2.chunkData.next_after)]]),
927
- {
928
- limit: 4
929
- }
930
- )
1208
+ bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch2.chunkData.next_after) }], {
1209
+ limit: 4
1210
+ })
931
1211
  );
932
1212
  expect(test_utils.getBatchData(batch3)).toEqual([]);
933
1213
 
@@ -938,7 +1218,8 @@ bucket_definitions:
938
1218
  const setup = async (options: BucketDataBatchOptions) => {
939
1219
  await using factory = await generateStorageFactory();
940
1220
  const syncRules = await factory.updateSyncRules(
941
- updateSyncRulesFromYaml(`
1221
+ updateSyncRulesFromYaml(
1222
+ `
942
1223
  bucket_definitions:
943
1224
  global1:
944
1225
  data:
@@ -946,50 +1227,46 @@ bucket_definitions:
946
1227
  global2:
947
1228
  data:
948
1229
  - SELECT id, description FROM test WHERE bucket = 'global2'
949
- `)
1230
+ `,
1231
+ { storageVersion }
1232
+ )
950
1233
  );
951
1234
  const bucketStorage = factory.getInstance(syncRules);
1235
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1236
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1237
+ await writer.markAllSnapshotDone('1/1');
952
1238
 
953
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
954
- const sourceTable = TEST_TABLE;
955
-
956
- for (let i = 1; i <= 10; i++) {
957
- await batch.save({
958
- sourceTable,
959
- tag: storage.SaveOperationTag.INSERT,
960
- after: {
961
- id: `test${i}`,
962
- description: `test${i}`,
963
- bucket: i == 1 ? 'global1' : 'global2'
964
- },
965
- afterReplicaId: `test${i}`
966
- });
967
- }
1239
+ for (let i = 1; i <= 10; i++) {
1240
+ await writer.save({
1241
+ sourceTable,
1242
+ tag: storage.SaveOperationTag.INSERT,
1243
+ after: {
1244
+ id: `test${i}`,
1245
+ description: `test${i}`,
1246
+ bucket: i == 1 ? 'global1' : 'global2'
1247
+ },
1248
+ afterReplicaId: `test${i}`
1249
+ });
1250
+ }
968
1251
 
969
- await batch.commit('1/1');
970
- });
1252
+ await writer.commit('1/1');
971
1253
 
972
1254
  const { checkpoint } = await bucketStorage.getCheckpoint();
1255
+ const global1Request = bucketRequest(syncRules, 'global1[]', 0n);
1256
+ const global2Request = bucketRequest(syncRules, 'global2[]', 0n);
973
1257
  const batch = await test_utils.fromAsync(
974
- bucketStorage.getBucketDataBatch(
975
- checkpoint,
976
- bucketRequestMap(syncRules, [
977
- ['global1[]', 0n],
978
- ['global2[]', 0n]
979
- ]),
980
- options
981
- )
1258
+ bucketStorage.getBucketDataBatch(checkpoint, [global1Request, global2Request], options)
982
1259
  );
983
1260
 
984
- return { syncRules, batch };
1261
+ return { batch, global1Request, global2Request };
985
1262
  };
986
1263
 
987
1264
  test('batch has_more (1)', async () => {
988
- const { batch, syncRules } = await setup({ limit: 5 });
1265
+ const { batch, global1Request, global2Request } = await setup({ limit: 5 });
989
1266
  expect(batch.length).toEqual(2);
990
1267
 
991
- expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]'));
992
- expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1268
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1269
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
993
1270
 
994
1271
  expect(test_utils.getBatchData(batch[0])).toEqual([
995
1272
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1016,11 +1293,11 @@ bucket_definitions:
1016
1293
  });
1017
1294
 
1018
1295
  test('batch has_more (2)', async () => {
1019
- const { batch, syncRules } = await setup({ limit: 11 });
1296
+ const { batch, global1Request, global2Request } = await setup({ limit: 11 });
1020
1297
  expect(batch.length).toEqual(2);
1021
1298
 
1022
- expect(batch[0].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global1[]'));
1023
- expect(batch[1].chunkData.bucket).toEqual(bucketRequest(syncRules, 'global2[]'));
1299
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1300
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
1024
1301
 
1025
1302
  expect(test_utils.getBatchData(batch[0])).toEqual([
1026
1303
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1053,12 +1330,12 @@ bucket_definitions:
1053
1330
 
1054
1331
  test('batch has_more (3)', async () => {
1055
1332
  // 50 bytes is more than 1 row, less than 2 rows
1056
- const { batch, syncRules } = await setup({ limit: 3, chunkLimitBytes: 50 });
1333
+ const { batch, global1Request, global2Request } = await setup({ limit: 3, chunkLimitBytes: 50 });
1057
1334
 
1058
1335
  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[]'));
1336
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1337
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
1338
+ expect(batch[2].chunkData.bucket).toEqual(global2Request.bucket);
1062
1339
 
1063
1340
  expect(test_utils.getBatchData(batch[0])).toEqual([
1064
1341
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -1102,9 +1379,9 @@ bucket_definitions:
1102
1379
 
1103
1380
  const r = await f.configureSyncRules(updateSyncRulesFromYaml('bucket_definitions: {}'));
1104
1381
  const storage = f.getInstance(r.persisted_sync_rules!);
1105
- await storage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1106
- await batch.keepalive('1/0');
1107
- });
1382
+ await using writer = await storage.createWriter(test_utils.BATCH_OPTIONS);
1383
+ await writer.markAllSnapshotDone('1/0');
1384
+ await writer.keepalive('1/0');
1108
1385
 
1109
1386
  await f.getStorageMetrics();
1110
1387
  // We don't care about the specific values here
@@ -1116,44 +1393,49 @@ bucket_definitions:
1116
1393
  // Similar to the above test, but splits over 1MB chunks.
1117
1394
  await using factory = await generateStorageFactory();
1118
1395
  const syncRules = await factory.updateSyncRules(
1119
- updateSyncRulesFromYaml(`
1396
+ updateSyncRulesFromYaml(
1397
+ `
1120
1398
  bucket_definitions:
1121
1399
  global:
1122
1400
  data:
1123
1401
  - SELECT id FROM test
1124
1402
  - SELECT id FROM test_ignore WHERE false
1125
- `)
1403
+ `,
1404
+ {
1405
+ storageVersion
1406
+ }
1407
+ )
1126
1408
  );
1127
1409
  const bucketStorage = factory.getInstance(syncRules);
1128
-
1129
- const sourceTable = test_utils.makeTestTable('test', ['id']);
1130
- const sourceTableIgnore = test_utils.makeTestTable('test_ignore', ['id']);
1131
-
1132
- const result1 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1133
- // This saves a record to current_data, but not bucket_data.
1134
- // This causes a checkpoint to be created without increasing the op_id sequence.
1135
- await batch.save({
1136
- sourceTable: sourceTableIgnore,
1137
- tag: storage.SaveOperationTag.INSERT,
1138
- after: {
1139
- id: 'test1'
1140
- },
1141
- afterReplicaId: test_utils.rid('test1')
1142
- });
1410
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1411
+
1412
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config, 1);
1413
+ const sourceTableIgnore = await test_utils.resolveTestTable(writer, 'test_ignore', ['id'], config, 2);
1414
+
1415
+ await writer.markAllSnapshotDone('1/1');
1416
+ // This saves a record to current_data, but not bucket_data.
1417
+ // This causes a checkpoint to be created without increasing the op_id sequence.
1418
+ await writer.save({
1419
+ sourceTable: sourceTableIgnore,
1420
+ tag: storage.SaveOperationTag.INSERT,
1421
+ after: {
1422
+ id: 'test1'
1423
+ },
1424
+ afterReplicaId: test_utils.rid('test1')
1143
1425
  });
1426
+ const result1 = await writer.flush();
1144
1427
 
1145
1428
  const checkpoint1 = result1!.flushed_op;
1146
1429
 
1147
- const result2 = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1148
- await batch.save({
1149
- sourceTable: sourceTable,
1150
- tag: storage.SaveOperationTag.INSERT,
1151
- after: {
1152
- id: 'test2'
1153
- },
1154
- afterReplicaId: test_utils.rid('test2')
1155
- });
1430
+ await writer.save({
1431
+ sourceTable: sourceTable,
1432
+ tag: storage.SaveOperationTag.INSERT,
1433
+ after: {
1434
+ id: 'test2'
1435
+ },
1436
+ afterReplicaId: test_utils.rid('test2')
1156
1437
  });
1438
+ const result2 = await writer.flush();
1157
1439
 
1158
1440
  const checkpoint2 = result2!.flushed_op;
1159
1441
  // we expect 0n and 1n, or 1n and 2n.
@@ -1163,41 +1445,242 @@ bucket_definitions:
1163
1445
  test('unchanged checksums', async () => {
1164
1446
  await using factory = await generateStorageFactory();
1165
1447
  const syncRules = await factory.updateSyncRules(
1166
- updateSyncRulesFromYaml(`
1448
+ updateSyncRulesFromYaml(
1449
+ `
1167
1450
  bucket_definitions:
1168
1451
  global:
1169
1452
  data:
1170
1453
  - SELECT client_id as id, description FROM "%"
1171
- `)
1454
+ `,
1455
+ {
1456
+ storageVersion
1457
+ }
1458
+ )
1172
1459
  );
1173
1460
  const bucketStorage = factory.getInstance(syncRules);
1174
-
1175
- const sourceTable = TEST_TABLE;
1176
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1177
- await batch.save({
1178
- sourceTable,
1179
- tag: storage.SaveOperationTag.INSERT,
1180
- after: {
1181
- id: 'test1',
1182
- description: 'test1a'
1183
- },
1184
- afterReplicaId: test_utils.rid('test1')
1185
- });
1186
- await batch.commit('1/1');
1461
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1462
+
1463
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1464
+ await writer.markAllSnapshotDone('1/1');
1465
+ await writer.save({
1466
+ sourceTable,
1467
+ tag: storage.SaveOperationTag.INSERT,
1468
+ after: {
1469
+ id: 'test1',
1470
+ description: 'test1a'
1471
+ },
1472
+ afterReplicaId: test_utils.rid('test1')
1187
1473
  });
1474
+ await writer.commit('1/1');
1188
1475
  const { checkpoint } = await bucketStorage.getCheckpoint();
1189
1476
 
1190
- const checksums = [
1191
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
1192
- ];
1193
- expect(checksums).toEqual([{ bucket: bucketRequest(syncRules, 'global[]'), checksum: 1917136889, count: 1 }]);
1194
- const checksums2 = [
1195
- ...(await bucketStorage.getChecksums(checkpoint + 1n, bucketRequests(syncRules, ['global[]']))).values()
1196
- ];
1197
- expect(checksums2).toEqual([{ bucket: bucketRequest(syncRules, 'global[]'), checksum: 1917136889, count: 1 }]);
1477
+ const request = bucketRequest(syncRules, 'global[]');
1478
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
1479
+ expect(checksums).toEqual([{ bucket: request.bucket, checksum: 1917136889, count: 1 }]);
1480
+ const checksums2 = [...(await bucketStorage.getChecksums(checkpoint + 1n, [request])).values()];
1481
+ expect(checksums2).toEqual([{ bucket: request.bucket, checksum: 1917136889, count: 1 }]);
1482
+ });
1483
+
1484
+ testChecksumBatching(config);
1485
+
1486
+ test('empty checkpoints (1)', async () => {
1487
+ await using factory = await generateStorageFactory();
1488
+ const syncRules = await factory.updateSyncRules(
1489
+ updateSyncRulesFromYaml(
1490
+ `
1491
+ bucket_definitions:
1492
+ global:
1493
+ data:
1494
+ - SELECT id, description FROM "%"
1495
+ `,
1496
+ { storageVersion }
1497
+ )
1498
+ );
1499
+ const bucketStorage = factory.getInstance(syncRules);
1500
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1501
+ await writer.markAllSnapshotDone('1/1');
1502
+ await writer.commit('1/1');
1503
+
1504
+ const cp1 = await bucketStorage.getCheckpoint();
1505
+ expect(cp1.lsn).toEqual('1/1');
1506
+
1507
+ await writer.commit('2/1', { createEmptyCheckpoints: true });
1508
+ const cp2 = await bucketStorage.getCheckpoint();
1509
+ expect(cp2.lsn).toEqual('2/1');
1510
+
1511
+ await writer.keepalive('3/1');
1512
+ const cp3 = await bucketStorage.getCheckpoint();
1513
+ expect(cp3.lsn).toEqual('3/1');
1514
+
1515
+ // For the last one, we skip creating empty checkpoints
1516
+ // This means the LSN stays at 3/1.
1517
+ await writer.commit('4/1', { createEmptyCheckpoints: false });
1518
+ const cp4 = await bucketStorage.getCheckpoint();
1519
+ expect(cp4.lsn).toEqual('3/1');
1520
+ });
1521
+
1522
+ test('empty checkpoints (2)', async () => {
1523
+ await using factory = await generateStorageFactory();
1524
+ const syncRules = await factory.updateSyncRules(
1525
+ updateSyncRulesFromYaml(
1526
+ `
1527
+ bucket_definitions:
1528
+ global:
1529
+ data:
1530
+ - SELECT id, description FROM "%"
1531
+ `,
1532
+ {
1533
+ storageVersion
1534
+ }
1535
+ )
1536
+ );
1537
+ const bucketStorage = factory.getInstance(syncRules);
1538
+ await using writer1 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1539
+ await using writer2 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1540
+ const sourceTable = await test_utils.resolveTestTable(writer2, 'test', ['id'], config);
1541
+
1542
+ // We simulate two concurrent batches, but sequential calls are enough for this test.
1543
+ await writer1.markAllSnapshotDone('1/1');
1544
+ await writer1.commit('1/1');
1545
+
1546
+ await writer1.commit('2/1', { createEmptyCheckpoints: false });
1547
+ const cp2 = await bucketStorage.getCheckpoint();
1548
+ expect(cp2.lsn).toEqual('1/1'); // checkpoint 2/1 skipped
1549
+
1550
+ await writer2.save({
1551
+ sourceTable,
1552
+ tag: storage.SaveOperationTag.INSERT,
1553
+ after: {
1554
+ id: 'test1',
1555
+ description: 'test1a'
1556
+ },
1557
+ afterReplicaId: test_utils.rid('test1')
1558
+ });
1559
+ // This simulates what happens on a snapshot processor.
1560
+ // This may later change to a flush() rather than commit().
1561
+ await writer2.commit(test_utils.BATCH_OPTIONS.zeroLSN);
1562
+
1563
+ const cp3 = await bucketStorage.getCheckpoint();
1564
+ expect(cp3.lsn).toEqual('1/1'); // Still unchanged
1565
+
1566
+ // This now needs to advance the LSN, despite {createEmptyCheckpoints: false}
1567
+ await writer1.commit('4/1', { createEmptyCheckpoints: false });
1568
+ const cp4 = await bucketStorage.getCheckpoint();
1569
+ expect(cp4.lsn).toEqual('4/1');
1570
+ });
1571
+
1572
+ test('empty checkpoints (sync rule activation)', async () => {
1573
+ await using factory = await generateStorageFactory();
1574
+ const syncRules = await factory.updateSyncRules(
1575
+ updateSyncRulesFromYaml(
1576
+ `
1577
+ bucket_definitions:
1578
+ global:
1579
+ data:
1580
+ - SELECT id, description FROM "%"
1581
+ `,
1582
+ {
1583
+ storageVersion
1584
+ }
1585
+ )
1586
+ );
1587
+ const bucketStorage = factory.getInstance(syncRules);
1588
+
1589
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1590
+ const result1 = await writer.commit('1/1', { createEmptyCheckpoints: false });
1591
+ expect(result1).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1592
+ // Snapshot is only valid once we reach 3/1
1593
+ await writer.markAllSnapshotDone('3/1');
1594
+
1595
+ // 2/1 < 3/1 - snapshot not valid yet, block checkpoint
1596
+ const result2 = await writer.commit('2/1', { createEmptyCheckpoints: false });
1597
+ expect(result2).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1598
+
1599
+ // No empty checkpoint should be created by the commit above.
1600
+ const cp1 = await bucketStorage.getCheckpoint();
1601
+ expect(cp1.lsn).toEqual(null);
1602
+
1603
+ // After this commit, the snapshot should be valid.
1604
+ // We specifically check that this is done even if createEmptyCheckpoints: false.
1605
+ const result3 = await writer.commit('3/1', { createEmptyCheckpoints: false });
1606
+ expect(result3).toEqual({ checkpointBlocked: false, checkpointCreated: true });
1607
+
1608
+ // Now, the checkpoint should advance the sync rules active.
1609
+ const cp2 = await bucketStorage.getCheckpoint();
1610
+ expect(cp2.lsn).toEqual('3/1');
1611
+
1612
+ const activeSyncRules = await factory.getActiveSyncRulesContent();
1613
+ expect(activeSyncRules?.id).toEqual(syncRules.id);
1614
+
1615
+ // At this point, it should be a truely empty checkpoint
1616
+ const result4 = await writer.commit('4/1', { createEmptyCheckpoints: false });
1617
+ expect(result4).toEqual({ checkpointBlocked: false, checkpointCreated: false });
1618
+
1619
+ // Unchanged
1620
+ const cp3 = await bucketStorage.getCheckpoint();
1621
+ expect(cp3.lsn).toEqual('3/1');
1198
1622
  });
1199
1623
 
1200
- testChecksumBatching(generateStorageFactory);
1624
+ test.runIf(storageVersion >= 3)('deleting while streaming', async () => {
1625
+ await using factory = await generateStorageFactory();
1626
+ const syncRules = await factory.updateSyncRules(
1627
+ updateSyncRulesFromYaml(
1628
+ `
1629
+ bucket_definitions:
1630
+ global:
1631
+ data:
1632
+ - SELECT id, description FROM "%"
1633
+ `,
1634
+ {
1635
+ storageVersion
1636
+ }
1637
+ )
1638
+ );
1639
+ const bucketStorage = factory.getInstance(syncRules);
1640
+ await using snapshotWriter = await bucketStorage.createWriter({
1641
+ ...test_utils.BATCH_OPTIONS,
1642
+ skipExistingRows: true
1643
+ });
1644
+ await using streamingWriter = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1645
+ const snapshotTable = await test_utils.resolveTestTable(snapshotWriter, 'test', ['id'], config, 1);
1646
+ const streamingTable = await test_utils.resolveTestTable(streamingWriter, 'test', ['id'], config, 1);
1647
+
1648
+ // We simulate two concurrent batches; separate writers are enough for this test.
1649
+ // For this test, we assume that we start with a row "test1", which is picked up by a snapshot
1650
+ // query, right before the delete is streamed. But the snapshot query is only persisted _after_
1651
+ // the delete is streamed, and we need to ensure that the streamed delete takes precedence.
1652
+ await streamingWriter.save({
1653
+ sourceTable: streamingTable,
1654
+ tag: storage.SaveOperationTag.DELETE,
1655
+ before: {
1656
+ id: 'test1'
1657
+ },
1658
+ beforeReplicaId: test_utils.rid('test1')
1659
+ });
1660
+ await streamingWriter.commit('2/1');
1661
+
1662
+ await snapshotWriter.save({
1663
+ sourceTable: snapshotTable,
1664
+ tag: storage.SaveOperationTag.INSERT,
1665
+ after: {
1666
+ id: 'test1',
1667
+ description: 'test1a'
1668
+ },
1669
+ afterReplicaId: test_utils.rid('test1')
1670
+ });
1671
+ await snapshotWriter.markAllSnapshotDone('3/1');
1672
+ await snapshotWriter.commit('1/1');
1673
+
1674
+ await streamingWriter.keepalive('3/1');
1675
+
1676
+ const cp = await bucketStorage.getCheckpoint();
1677
+ expect(cp.lsn).toEqual('3/1');
1678
+ const data = await test_utils.fromAsync(
1679
+ bucketStorage.getBucketDataBatch(cp.checkpoint, [bucketRequest(syncRules, 'global[]')])
1680
+ );
1681
+
1682
+ expect(data).toEqual([]);
1683
+ });
1201
1684
  }
1202
1685
 
1203
1686
  /**
@@ -1205,50 +1688,58 @@ bucket_definitions:
1205
1688
  *
1206
1689
  * Exposed as a separate test so we can test with more storage parameters.
1207
1690
  */
1208
- export function testChecksumBatching(generateStorageFactory: storage.TestStorageFactory) {
1691
+ export function testChecksumBatching(config: storage.TestStorageConfig) {
1692
+ const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
1209
1693
  test('checksums for multiple buckets', async () => {
1210
- await using factory = await generateStorageFactory();
1694
+ await using factory = await config.factory();
1211
1695
  const syncRules = await factory.updateSyncRules(
1212
- updateSyncRulesFromYaml(`
1696
+ updateSyncRulesFromYaml(
1697
+ `
1213
1698
  bucket_definitions:
1214
1699
  user:
1215
1700
  parameters: select request.user_id() as user_id
1216
1701
  data:
1217
1702
  - select id, description from test where user_id = bucket.user_id
1218
- `)
1703
+ `,
1704
+ {
1705
+ storageVersion
1706
+ }
1707
+ )
1219
1708
  );
1220
1709
  const bucketStorage = factory.getInstance(syncRules);
1221
-
1222
- const sourceTable = TEST_TABLE;
1223
- await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
1224
- for (let u of ['u1', 'u2', 'u3', 'u4']) {
1225
- for (let t of ['t1', 't2', 't3', 't4']) {
1226
- const id = `${t}_${u}`;
1227
- await batch.save({
1228
- sourceTable,
1229
- tag: storage.SaveOperationTag.INSERT,
1230
- after: {
1231
- id,
1232
- description: `${t} description`,
1233
- user_id: u
1234
- },
1235
- afterReplicaId: test_utils.rid(id)
1236
- });
1237
- }
1710
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1711
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1712
+ await writer.markAllSnapshotDone('1/1');
1713
+ for (let u of ['u1', 'u2', 'u3', 'u4']) {
1714
+ for (let t of ['t1', 't2', 't3', 't4']) {
1715
+ const id = `${t}_${u}`;
1716
+ await writer.save({
1717
+ sourceTable,
1718
+ tag: storage.SaveOperationTag.INSERT,
1719
+ after: {
1720
+ id,
1721
+ description: `${t} description`,
1722
+ user_id: u
1723
+ },
1724
+ afterReplicaId: test_utils.rid(id)
1725
+ });
1238
1726
  }
1239
- await batch.commit('1/1');
1240
- });
1727
+ }
1728
+ await writer.commit('1/1');
1241
1729
  const { checkpoint } = await bucketStorage.getCheckpoint();
1242
1730
 
1243
1731
  bucketStorage.clearChecksumCache();
1244
- const buckets = bucketRequests(syncRules, ['user["u1"]', 'user["u2"]', 'user["u3"]', 'user["u4"]']);
1245
- const checksums = [...(await bucketStorage.getChecksums(checkpoint, buckets)).values()];
1732
+ const users = ['u1', 'u2', 'u3', 'u4'];
1733
+ const expectedChecksums = [346204588, 5261081, 134760718, -302639724];
1734
+ const bucketRequests = users.map((user) => bucketRequest(syncRules, `user["${user}"]`));
1735
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, bucketRequests)).values()];
1246
1736
  checksums.sort((a, b) => a.bucket.localeCompare(b.bucket));
1247
- 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 }
1252
- ]);
1737
+ const expected = bucketRequests.map((request, index) => ({
1738
+ bucket: request.bucket,
1739
+ count: 4,
1740
+ checksum: expectedChecksums[index]
1741
+ }));
1742
+ expected.sort((a, b) => a.bucket.localeCompare(b.bucket));
1743
+ expect(checksums).toEqual(expected);
1253
1744
  });
1254
1745
  }