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