@powersync/service-core-tests 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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 (55) hide show
  1. package/CHANGELOG.md +95 -3
  2. package/dist/test-utils/MetricsHelper.js +1 -1
  3. package/dist/test-utils/MetricsHelper.js.map +1 -1
  4. package/dist/test-utils/StorageDataHelpers.d.ts +8 -0
  5. package/dist/test-utils/StorageDataHelpers.js +33 -0
  6. package/dist/test-utils/StorageDataHelpers.js.map +1 -0
  7. package/dist/test-utils/general-utils.d.ts +22 -3
  8. package/dist/test-utils/general-utils.js +56 -3
  9. package/dist/test-utils/general-utils.js.map +1 -1
  10. package/dist/test-utils/stream_utils.js +2 -2
  11. package/dist/test-utils/stream_utils.js.map +1 -1
  12. package/dist/test-utils/test-utils-index.d.ts +1 -0
  13. package/dist/test-utils/test-utils-index.js +1 -0
  14. package/dist/test-utils/test-utils-index.js.map +1 -1
  15. package/dist/tests/register-compacting-tests.d.ts +1 -1
  16. package/dist/tests/register-compacting-tests.js +514 -587
  17. package/dist/tests/register-compacting-tests.js.map +1 -1
  18. package/dist/tests/register-data-storage-checkpoint-tests.d.ts +1 -1
  19. package/dist/tests/register-data-storage-checkpoint-tests.js +200 -301
  20. package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
  21. package/dist/tests/register-data-storage-data-tests.d.ts +2 -2
  22. package/dist/tests/register-data-storage-data-tests.js +1220 -1041
  23. package/dist/tests/register-data-storage-data-tests.js.map +1 -1
  24. package/dist/tests/register-data-storage-parameter-tests.d.ts +1 -1
  25. package/dist/tests/register-data-storage-parameter-tests.js +681 -632
  26. package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
  27. package/dist/tests/register-migration-tests.js +63 -139
  28. package/dist/tests/register-migration-tests.js.map +1 -1
  29. package/dist/tests/register-parameter-compacting-tests.d.ts +1 -1
  30. package/dist/tests/register-parameter-compacting-tests.js +121 -201
  31. package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
  32. package/dist/tests/register-sync-tests.d.ts +2 -1
  33. package/dist/tests/register-sync-tests.js +974 -1136
  34. package/dist/tests/register-sync-tests.js.map +1 -1
  35. package/dist/tests/tests-index.d.ts +4 -4
  36. package/dist/tests/tests-index.js +4 -4
  37. package/dist/tests/tests-index.js.map +1 -1
  38. package/dist/tests/util.d.ts +5 -4
  39. package/dist/tests/util.js +27 -12
  40. package/dist/tests/util.js.map +1 -1
  41. package/package.json +6 -6
  42. package/src/test-utils/MetricsHelper.ts +1 -1
  43. package/src/test-utils/StorageDataHelpers.ts +44 -0
  44. package/src/test-utils/general-utils.ts +81 -4
  45. package/src/test-utils/stream_utils.ts +2 -2
  46. package/src/test-utils/test-utils-index.ts +1 -0
  47. package/src/tests/register-compacting-tests.ts +376 -322
  48. package/src/tests/register-data-storage-checkpoint-tests.ts +85 -53
  49. package/src/tests/register-data-storage-data-tests.ts +1076 -563
  50. package/src/tests/register-data-storage-parameter-tests.ts +631 -327
  51. package/src/tests/register-parameter-compacting-tests.ts +92 -96
  52. package/src/tests/register-sync-tests.ts +463 -379
  53. package/src/tests/tests-index.ts +4 -4
  54. package/src/tests/util.ts +46 -17
  55. package/tsconfig.tsbuildinfo +1 -1
@@ -1,59 +1,7 @@
1
- var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
- if (value !== null && value !== void 0) {
3
- if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
- var dispose, inner;
5
- if (async) {
6
- if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
- dispose = value[Symbol.asyncDispose];
8
- }
9
- if (dispose === void 0) {
10
- if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
- dispose = value[Symbol.dispose];
12
- if (async) inner = dispose;
13
- }
14
- if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
- if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
- env.stack.push({ value: value, dispose: dispose, async: async });
17
- }
18
- else if (async) {
19
- env.stack.push({ async: true });
20
- }
21
- return value;
22
- };
23
- var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
- return function (env) {
25
- function fail(e) {
26
- env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
- env.hasError = true;
28
- }
29
- var r, s = 0;
30
- function next() {
31
- while (r = env.stack.pop()) {
32
- try {
33
- if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
- if (r.dispose) {
35
- var result = r.dispose.call(r.value);
36
- if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
- }
38
- else s |= 1;
39
- }
40
- catch (e) {
41
- fail(e);
42
- }
43
- }
44
- if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
- if (env.hasError) throw env.error;
46
- }
47
- return next();
48
- };
49
- })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
- var e = new Error(message);
51
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
- });
53
- import { getUuidReplicaIdentityBson, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
1
+ import { CURRENT_STORAGE_VERSION, getUuidReplicaIdentityBson, reduceBucket, storage, updateSyncRulesFromYaml } from '@powersync/service-core';
54
2
  import { describe, expect, test } from 'vitest';
55
3
  import * as test_utils from '../test-utils/test-utils-index.js';
56
- import { bucketRequest, bucketRequestMap, bucketRequests, TEST_TABLE } from './util.js';
4
+ import { bucketRequest } from '../test-utils/test-utils-index.js';
57
5
  /**
58
6
  * Normalize data from OplogEntries for comparison in tests.
59
7
  * Tests typically expect the stringified result
@@ -74,856 +22,969 @@ const normalizeOplogData = (data) => {
74
22
  *
75
23
  * ```
76
24
  */
77
- export function registerDataStorageDataTests(generateStorageFactory) {
25
+ export function registerDataStorageDataTests(config) {
26
+ const generateStorageFactory = config.factory;
27
+ const storageVersion = config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
78
28
  test('removing row', async () => {
79
- const env_1 = { stack: [], error: void 0, hasError: false };
80
- try {
81
- const factory = __addDisposableResource(env_1, await generateStorageFactory(), true);
82
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
29
+ await using factory = await generateStorageFactory();
30
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
83
31
  bucket_definitions:
84
32
  global:
85
33
  data:
86
34
  - SELECT id, description FROM "%"
87
- `));
88
- 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');
35
+ `, { storageVersion }));
36
+ const bucketStorage = factory.getInstance(syncRules);
37
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
38
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
39
+ await writer.markAllSnapshotDone('1/1');
40
+ await writer.save({
41
+ sourceTable: testTable,
42
+ tag: storage.SaveOperationTag.INSERT,
43
+ after: {
44
+ id: 'test1',
45
+ description: 'test1'
46
+ },
47
+ afterReplicaId: test_utils.rid('test1')
48
+ });
49
+ await writer.save({
50
+ sourceTable: testTable,
51
+ tag: storage.SaveOperationTag.DELETE,
52
+ beforeReplicaId: test_utils.rid('test1')
53
+ });
54
+ await writer.commit('1/1');
55
+ const { checkpoint } = await bucketStorage.getCheckpoint();
56
+ const request = bucketRequest(syncRules, 'global[]');
57
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
58
+ const data = batch[0].chunkData.data.map((d) => {
59
+ return {
60
+ op: d.op,
61
+ object_id: d.object_id,
62
+ checksum: d.checksum
63
+ };
64
+ });
65
+ const c1 = 2871785649;
66
+ const c2 = 2872534815;
67
+ expect(data).toEqual([
68
+ { op: 'PUT', object_id: 'test1', checksum: c1 },
69
+ { op: 'REMOVE', object_id: 'test1', checksum: c2 }
70
+ ]);
71
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
72
+ expect(checksums).toEqual([
73
+ {
74
+ bucket: request.bucket,
75
+ checksum: (c1 + c2) & 0xffffffff,
76
+ count: 2
77
+ }
78
+ ]);
79
+ });
80
+ test('insert after delete in new batch', async () => {
81
+ await using factory = await generateStorageFactory();
82
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
83
+ bucket_definitions:
84
+ global:
85
+ data:
86
+ - SELECT id, description FROM "%"
87
+ `, { storageVersion }));
88
+ const bucketStorage = factory.getInstance(syncRules);
89
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
90
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
91
+ await writer.markAllSnapshotDone('1/1');
92
+ await writer.save({
93
+ sourceTable: testTable,
94
+ tag: storage.SaveOperationTag.DELETE,
95
+ beforeReplicaId: test_utils.rid('test1')
96
+ });
97
+ await writer.commit('0/1');
98
+ await writer.save({
99
+ sourceTable: testTable,
100
+ tag: storage.SaveOperationTag.INSERT,
101
+ after: {
102
+ id: 'test1',
103
+ description: 'test1'
104
+ },
105
+ afterReplicaId: test_utils.rid('test1')
106
+ });
107
+ await writer.commit('2/1');
108
+ const { checkpoint } = await bucketStorage.getCheckpoint();
109
+ const request = bucketRequest(syncRules, 'global[]');
110
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
111
+ const data = batch[0].chunkData.data.map((d) => {
112
+ return {
113
+ op: d.op,
114
+ object_id: d.object_id,
115
+ checksum: d.checksum
116
+ };
117
+ });
118
+ const c1 = 2871785649;
119
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
120
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
121
+ expect(checksums).toEqual([
122
+ {
123
+ bucket: request.bucket,
124
+ checksum: c1 & 0xffffffff,
125
+ count: 1
126
+ }
127
+ ]);
128
+ });
129
+ test('update after delete in new batch', async () => {
130
+ // Update after delete may not be common, but the storage layer should handle it in an eventually-consistent way.
131
+ await using factory = await generateStorageFactory();
132
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
133
+ bucket_definitions:
134
+ global:
135
+ data:
136
+ - SELECT id, description FROM "%"
137
+ `, { storageVersion }));
138
+ const bucketStorage = factory.getInstance(syncRules);
139
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
140
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
141
+ await writer.markAllSnapshotDone('1/1');
142
+ await writer.save({
143
+ sourceTable: testTable,
144
+ tag: storage.SaveOperationTag.DELETE,
145
+ beforeReplicaId: test_utils.rid('test1')
146
+ });
147
+ await writer.commit('0/1');
148
+ await writer.save({
149
+ sourceTable: testTable,
150
+ tag: storage.SaveOperationTag.UPDATE,
151
+ before: {
152
+ id: 'test1'
153
+ },
154
+ after: {
155
+ id: 'test1',
156
+ description: 'test1'
157
+ },
158
+ beforeReplicaId: test_utils.rid('test1'),
159
+ afterReplicaId: test_utils.rid('test1')
160
+ });
161
+ await writer.commit('2/1');
162
+ const { checkpoint } = await bucketStorage.getCheckpoint();
163
+ const request = bucketRequest(syncRules, 'global[]');
164
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
165
+ const data = batch[0].chunkData.data.map((d) => {
166
+ return {
167
+ op: d.op,
168
+ object_id: d.object_id,
169
+ checksum: d.checksum
170
+ };
171
+ });
172
+ const c1 = 2871785649;
173
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
174
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
175
+ expect(checksums).toEqual([
176
+ {
177
+ bucket: request.bucket,
178
+ checksum: c1 & 0xffffffff,
179
+ count: 1
180
+ }
181
+ ]);
182
+ });
183
+ test('insert after delete in same batch', async () => {
184
+ await using factory = await generateStorageFactory();
185
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
186
+ bucket_definitions:
187
+ global:
188
+ data:
189
+ - SELECT id, description FROM "%"
190
+ `, {
191
+ storageVersion
192
+ }));
193
+ const bucketStorage = factory.getInstance(syncRules);
194
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
195
+ const testTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
196
+ await writer.markAllSnapshotDone('1/1');
197
+ await writer.save({
198
+ sourceTable: testTable,
199
+ tag: storage.SaveOperationTag.DELETE,
200
+ beforeReplicaId: test_utils.rid('test1')
201
+ });
202
+ await writer.save({
203
+ sourceTable: testTable,
204
+ tag: storage.SaveOperationTag.INSERT,
205
+ after: {
206
+ id: 'test1',
207
+ description: 'test1'
208
+ },
209
+ afterReplicaId: test_utils.rid('test1')
210
+ });
211
+ await writer.commit('1/1');
212
+ const { checkpoint } = await bucketStorage.getCheckpoint();
213
+ const request = bucketRequest(syncRules, 'global[]');
214
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
215
+ const data = batch[0].chunkData.data.map((d) => {
216
+ return {
217
+ op: d.op,
218
+ object_id: d.object_id,
219
+ checksum: d.checksum
220
+ };
221
+ });
222
+ const c1 = 2871785649;
223
+ expect(data).toEqual([{ op: 'PUT', object_id: 'test1', checksum: c1 }]);
224
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
225
+ expect(checksums).toEqual([
226
+ {
227
+ bucket: request.bucket,
228
+ checksum: c1 & 0xffffffff,
229
+ count: 1
230
+ }
231
+ ]);
232
+ });
233
+ test('(insert, delete, insert), (delete)', async () => {
234
+ await using factory = await generateStorageFactory();
235
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
236
+ bucket_definitions:
237
+ global:
238
+ data:
239
+ - SELECT id, description FROM "%"
240
+ `, {
241
+ storageVersion
242
+ }));
243
+ const bucketStorage = factory.getInstance(syncRules);
244
+ {
245
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
246
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
247
+ await writer.markAllSnapshotDone('1/1');
248
+ await writer.save({
249
+ sourceTable,
250
+ tag: storage.SaveOperationTag.INSERT,
251
+ after: {
252
+ id: 'test1',
253
+ description: 'test1'
254
+ },
255
+ afterReplicaId: test_utils.rid('test1')
106
256
  });
107
- const { checkpoint } = await bucketStorage.getCheckpoint();
108
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]])));
109
- const data = batch[0].chunkData.data.map((d) => {
110
- return {
111
- op: d.op,
112
- object_id: d.object_id,
113
- checksum: d.checksum
114
- };
257
+ await writer.save({
258
+ sourceTable,
259
+ tag: storage.SaveOperationTag.DELETE,
260
+ beforeReplicaId: test_utils.rid('test1')
115
261
  });
116
- const c1 = 2871785649;
117
- const c2 = 2872534815;
118
- expect(data).toEqual([
119
- { op: 'PUT', object_id: 'test1', checksum: c1 },
120
- { op: 'REMOVE', object_id: 'test1', checksum: c2 }
121
- ]);
122
- const checksums = [
123
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
124
- ];
125
- expect(checksums).toEqual([
126
- {
127
- bucket: bucketRequest(syncRules, 'global[]'),
128
- checksum: (c1 + c2) & 0xffffffff,
129
- count: 2
130
- }
131
- ]);
132
- }
133
- catch (e_1) {
134
- env_1.error = e_1;
135
- env_1.hasError = true;
262
+ await writer.save({
263
+ sourceTable,
264
+ tag: storage.SaveOperationTag.INSERT,
265
+ after: {
266
+ id: 'test1',
267
+ description: 'test1'
268
+ },
269
+ afterReplicaId: test_utils.rid('test1')
270
+ });
271
+ await writer.commit('1/1');
136
272
  }
137
- finally {
138
- const result_1 = __disposeResources(env_1);
139
- if (result_1)
140
- await result_1;
273
+ {
274
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
275
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
276
+ await writer.markAllSnapshotDone('1/1');
277
+ await writer.save({
278
+ sourceTable,
279
+ tag: storage.SaveOperationTag.DELETE,
280
+ beforeReplicaId: test_utils.rid('test1')
281
+ });
282
+ await writer.commit('2/1');
141
283
  }
284
+ const { checkpoint } = await bucketStorage.getCheckpoint();
285
+ const request = bucketRequest(syncRules, 'global[]');
286
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
287
+ expect(reduceBucket(batch[0].chunkData.data).slice(1)).toEqual([]);
288
+ const data = batch[0].chunkData.data.map((d) => {
289
+ return {
290
+ op: d.op,
291
+ object_id: d.object_id,
292
+ checksum: d.checksum
293
+ };
294
+ });
295
+ expect(data).toMatchSnapshot();
142
296
  });
143
297
  test('changing client ids', async () => {
144
- const env_2 = { stack: [], error: void 0, hasError: false };
145
- try {
146
- const factory = __addDisposableResource(env_2, await generateStorageFactory(), true);
147
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
298
+ await using factory = await generateStorageFactory();
299
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
148
300
  bucket_definitions:
149
301
  global:
150
302
  data:
151
303
  - SELECT client_id as id, description FROM "%"
152
- `));
153
- 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');
187
- });
188
- const { checkpoint } = await bucketStorage.getCheckpoint();
189
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]])));
190
- const data = batch[0].chunkData.data.map((d) => {
191
- return {
192
- op: d.op,
193
- object_id: d.object_id
194
- };
195
- });
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' }
201
- ]);
202
- }
203
- catch (e_2) {
204
- env_2.error = e_2;
205
- env_2.hasError = true;
206
- }
207
- finally {
208
- const result_2 = __disposeResources(env_2);
209
- if (result_2)
210
- await result_2;
211
- }
304
+ `, {
305
+ storageVersion
306
+ }));
307
+ const bucketStorage = factory.getInstance(syncRules);
308
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
309
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
310
+ await writer.markAllSnapshotDone('1/1');
311
+ await writer.save({
312
+ sourceTable,
313
+ tag: storage.SaveOperationTag.INSERT,
314
+ after: {
315
+ id: 'test1',
316
+ client_id: 'client1a',
317
+ description: 'test1a'
318
+ },
319
+ afterReplicaId: test_utils.rid('test1')
320
+ });
321
+ await writer.save({
322
+ sourceTable,
323
+ tag: storage.SaveOperationTag.UPDATE,
324
+ after: {
325
+ id: 'test1',
326
+ client_id: 'client1b',
327
+ description: 'test1b'
328
+ },
329
+ afterReplicaId: test_utils.rid('test1')
330
+ });
331
+ await writer.save({
332
+ sourceTable,
333
+ tag: storage.SaveOperationTag.INSERT,
334
+ after: {
335
+ id: 'test2',
336
+ client_id: 'client2',
337
+ description: 'test2'
338
+ },
339
+ afterReplicaId: test_utils.rid('test2')
340
+ });
341
+ await writer.commit('1/1');
342
+ const { checkpoint } = await bucketStorage.getCheckpoint();
343
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [bucketRequest(syncRules, 'global[]')]));
344
+ const data = batch[0].chunkData.data.map((d) => {
345
+ return {
346
+ op: d.op,
347
+ object_id: d.object_id
348
+ };
349
+ });
350
+ expect(data).toEqual([
351
+ { op: 'PUT', object_id: 'client1a' },
352
+ { op: 'PUT', object_id: 'client1b' },
353
+ { op: 'REMOVE', object_id: 'client1a' },
354
+ { op: 'PUT', object_id: 'client2' }
355
+ ]);
212
356
  });
213
357
  test('re-apply delete', async () => {
214
- const env_3 = { stack: [], error: void 0, hasError: false };
215
- try {
216
- const factory = __addDisposableResource(env_3, await generateStorageFactory(), true);
217
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
358
+ await using factory = await generateStorageFactory();
359
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
218
360
  bucket_definitions:
219
361
  global:
220
362
  data:
221
363
  - SELECT id, description FROM "%"
222
- `));
223
- 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
- });
235
- });
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');
244
- });
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
- });
252
- });
253
- const { checkpoint } = await bucketStorage.getCheckpoint();
254
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]])));
255
- const data = batch[0].chunkData.data.map((d) => {
256
- return {
257
- op: d.op,
258
- object_id: d.object_id,
259
- checksum: d.checksum
260
- };
261
- });
262
- const c1 = 2871785649;
263
- const c2 = 2872534815;
264
- expect(data).toEqual([
265
- { op: 'PUT', object_id: 'test1', checksum: c1 },
266
- { op: 'REMOVE', object_id: 'test1', checksum: c2 }
267
- ]);
268
- const checksums = [
269
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
270
- ];
271
- expect(checksums).toEqual([
272
- {
273
- bucket: bucketRequest(syncRules, 'global[]'),
274
- checksum: (c1 + c2) & 0xffffffff,
275
- count: 2
276
- }
277
- ]);
278
- }
279
- catch (e_3) {
280
- env_3.error = e_3;
281
- env_3.hasError = true;
282
- }
283
- finally {
284
- const result_3 = __disposeResources(env_3);
285
- if (result_3)
286
- await result_3;
287
- }
364
+ `, {
365
+ storageVersion
366
+ }));
367
+ const bucketStorage = factory.getInstance(syncRules);
368
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
369
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
370
+ await writer.markAllSnapshotDone('1/1');
371
+ await writer.save({
372
+ sourceTable,
373
+ tag: storage.SaveOperationTag.INSERT,
374
+ after: {
375
+ id: 'test1',
376
+ description: 'test1'
377
+ },
378
+ afterReplicaId: test_utils.rid('test1')
379
+ });
380
+ await writer.flush();
381
+ await writer.save({
382
+ sourceTable,
383
+ tag: storage.SaveOperationTag.DELETE,
384
+ beforeReplicaId: test_utils.rid('test1')
385
+ });
386
+ await writer.commit('1/1');
387
+ await writer.save({
388
+ sourceTable,
389
+ tag: storage.SaveOperationTag.DELETE,
390
+ beforeReplicaId: test_utils.rid('test1')
391
+ });
392
+ await writer.flush();
393
+ const { checkpoint } = await bucketStorage.getCheckpoint();
394
+ const request = bucketRequest(syncRules, 'global[]');
395
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
396
+ const data = batch[0].chunkData.data.map((d) => {
397
+ return {
398
+ op: d.op,
399
+ object_id: d.object_id,
400
+ checksum: d.checksum
401
+ };
402
+ });
403
+ const c1 = 2871785649;
404
+ const c2 = 2872534815;
405
+ expect(data).toEqual([
406
+ { op: 'PUT', object_id: 'test1', checksum: c1 },
407
+ { op: 'REMOVE', object_id: 'test1', checksum: c2 }
408
+ ]);
409
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
410
+ expect(checksums).toEqual([
411
+ {
412
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
413
+ checksum: (c1 + c2) & 0xffffffff,
414
+ count: 2
415
+ }
416
+ ]);
288
417
  });
289
418
  test('re-apply update + delete', async () => {
290
- const env_4 = { stack: [], error: void 0, hasError: false };
291
- try {
292
- const factory = __addDisposableResource(env_4, await generateStorageFactory(), true);
293
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
419
+ await using factory = await generateStorageFactory();
420
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
294
421
  bucket_definitions:
295
422
  global:
296
423
  data:
297
424
  - SELECT id, description FROM "%"
298
- `));
299
- 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
- });
311
- });
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');
338
- });
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');
365
- });
366
- const { checkpoint } = await bucketStorage.getCheckpoint();
367
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]])));
368
- const data = batch[0].chunkData.data.map((d) => {
369
- return {
370
- op: d.op,
371
- object_id: d.object_id,
372
- checksum: d.checksum
373
- };
374
- });
375
- const c1 = 2871785649;
376
- const c2 = 2872534815;
377
- expect(data).toEqual([
378
- { op: 'PUT', object_id: 'test1', checksum: c1 },
379
- { op: 'PUT', object_id: 'test1', checksum: c1 },
380
- { op: 'PUT', object_id: 'test1', checksum: c1 },
381
- { op: 'REMOVE', object_id: 'test1', checksum: c2 }
382
- ]);
383
- const checksums = [
384
- ...(await bucketStorage.getChecksums(checkpoint, bucketRequests(syncRules, ['global[]']))).values()
385
- ];
386
- expect(checksums).toEqual([
387
- {
388
- bucket: bucketRequest(syncRules, 'global[]'),
389
- checksum: (c1 + c1 + c1 + c2) & 0xffffffff,
390
- count: 4
391
- }
392
- ]);
393
- }
394
- catch (e_4) {
395
- env_4.error = e_4;
396
- env_4.hasError = true;
397
- }
398
- finally {
399
- const result_4 = __disposeResources(env_4);
400
- if (result_4)
401
- await result_4;
402
- }
425
+ `, { storageVersion }));
426
+ const bucketStorage = factory.getInstance(syncRules);
427
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
428
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
429
+ await writer.markAllSnapshotDone('1/1');
430
+ await writer.save({
431
+ sourceTable,
432
+ tag: storage.SaveOperationTag.INSERT,
433
+ after: {
434
+ id: 'test1',
435
+ description: 'test1'
436
+ },
437
+ afterReplicaId: test_utils.rid('test1')
438
+ });
439
+ await writer.flush();
440
+ await writer.markAllSnapshotDone('1/1');
441
+ await writer.save({
442
+ sourceTable,
443
+ tag: storage.SaveOperationTag.UPDATE,
444
+ after: {
445
+ id: 'test1',
446
+ description: undefined
447
+ },
448
+ afterReplicaId: test_utils.rid('test1')
449
+ });
450
+ await writer.save({
451
+ sourceTable,
452
+ tag: storage.SaveOperationTag.UPDATE,
453
+ after: {
454
+ id: 'test1',
455
+ description: undefined
456
+ },
457
+ afterReplicaId: test_utils.rid('test1')
458
+ });
459
+ await writer.save({
460
+ sourceTable,
461
+ tag: storage.SaveOperationTag.DELETE,
462
+ beforeReplicaId: test_utils.rid('test1')
463
+ });
464
+ await writer.commit('1/1');
465
+ await writer.markAllSnapshotDone('1/1');
466
+ await writer.save({
467
+ sourceTable,
468
+ tag: storage.SaveOperationTag.UPDATE,
469
+ after: {
470
+ id: 'test1',
471
+ description: undefined
472
+ },
473
+ afterReplicaId: test_utils.rid('test1')
474
+ });
475
+ await writer.save({
476
+ sourceTable,
477
+ tag: storage.SaveOperationTag.UPDATE,
478
+ after: {
479
+ id: 'test1',
480
+ description: undefined
481
+ },
482
+ afterReplicaId: test_utils.rid('test1')
483
+ });
484
+ await writer.save({
485
+ sourceTable,
486
+ tag: storage.SaveOperationTag.DELETE,
487
+ beforeReplicaId: test_utils.rid('test1')
488
+ });
489
+ await writer.commit('2/1');
490
+ const { checkpoint } = await bucketStorage.getCheckpoint();
491
+ const request = bucketRequest(syncRules, 'global[]');
492
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request]));
493
+ const data = batch[0].chunkData.data.map((d) => {
494
+ return {
495
+ op: d.op,
496
+ object_id: d.object_id,
497
+ checksum: d.checksum
498
+ };
499
+ });
500
+ const c1 = 2871785649;
501
+ const c2 = 2872534815;
502
+ expect(data).toEqual([
503
+ { op: 'PUT', object_id: 'test1', checksum: c1 },
504
+ { op: 'PUT', object_id: 'test1', checksum: c1 },
505
+ { op: 'PUT', object_id: 'test1', checksum: c1 },
506
+ { op: 'REMOVE', object_id: 'test1', checksum: c2 }
507
+ ]);
508
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
509
+ expect(checksums).toEqual([
510
+ {
511
+ bucket: bucketRequest(syncRules, 'global[]').bucket,
512
+ checksum: (c1 + c1 + c1 + c2) & 0xffffffff,
513
+ count: 4
514
+ }
515
+ ]);
403
516
  });
404
517
  test('batch with overlapping replica ids', async () => {
405
- const env_5 = { stack: [], error: void 0, hasError: false };
406
- try {
407
- // This test checks that we get the correct output when processing rows with:
408
- // 1. changing replica ids
409
- // 2. overlapping with replica ids of other rows in the same transaction (at different times)
410
- // If operations are not processing in input order, this breaks easily.
411
- // It can break at two places:
412
- // 1. Not getting the correct "current_data" state for each operation.
413
- // 2. Output order not being correct.
414
- const factory = __addDisposableResource(env_5, await generateStorageFactory(), true);
415
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
518
+ // This test checks that we get the correct output when processing rows with:
519
+ // 1. changing replica ids
520
+ // 2. overlapping with replica ids of other rows in the same transaction (at different times)
521
+ // If operations are not processing in input order, this breaks easily.
522
+ // It can break at two places:
523
+ // 1. Not getting the correct "current_data" state for each operation.
524
+ // 2. Output order not being correct.
525
+ await using factory = await generateStorageFactory();
526
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
416
527
  bucket_definitions:
417
528
  global:
418
529
  data:
419
530
  - SELECT id, description FROM "test"
420
- `));
421
- const bucketStorage = factory.getInstance(syncRules);
422
- // 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
- });
443
- });
444
- const checkpoint1 = result1?.flushed_op ?? 0n;
445
- // 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
- });
517
- });
518
- const checkpoint2 = result2.flushed_op;
519
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint2, bucketRequestMap(syncRules, [['global[]', checkpoint1]])));
520
- const data = batch[0].chunkData.data.map((d) => {
521
- return {
522
- op: d.op,
523
- object_id: d.object_id,
524
- data: normalizeOplogData(d.data)
525
- };
526
- });
527
- // Operations must be in this order
528
- expect(data).toEqual([
529
- // b
530
- { op: 'PUT', object_id: 'test1', data: JSON.stringify({ id: 'test1', description: 'test1b' }) },
531
- { op: 'REMOVE', object_id: 'test1', data: null },
532
- { op: 'PUT', object_id: 'test2', data: JSON.stringify({ id: 'test2', description: 'test2b' }) },
533
- { op: 'REMOVE', object_id: 'test2', data: null },
534
- { op: 'PUT', object_id: 'test3', data: JSON.stringify({ id: 'test3', description: 'test3b' }) },
535
- // c
536
- { op: 'PUT', object_id: 'test2', data: JSON.stringify({ id: 'test2', description: 'test2c' }) },
537
- // d
538
- { op: 'PUT', object_id: 'test4', data: JSON.stringify({ id: 'test4', description: 'test4d' }) },
539
- { op: 'REMOVE', object_id: 'test4', data: null },
540
- { op: 'PUT', object_id: 'test5', data: JSON.stringify({ id: 'test5', description: 'test5d' }) }
541
- ]);
542
- }
543
- catch (e_5) {
544
- env_5.error = e_5;
545
- env_5.hasError = true;
546
- }
547
- finally {
548
- const result_5 = __disposeResources(env_5);
549
- if (result_5)
550
- await result_5;
551
- }
531
+ `, { storageVersion }));
532
+ const bucketStorage = factory.getInstance(syncRules);
533
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
534
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
535
+ // Pre-setup
536
+ await writer.markAllSnapshotDone('1/1');
537
+ await writer.save({
538
+ sourceTable,
539
+ tag: storage.SaveOperationTag.INSERT,
540
+ after: {
541
+ id: 'test1',
542
+ description: 'test1a'
543
+ },
544
+ afterReplicaId: test_utils.rid('test1')
545
+ });
546
+ await writer.save({
547
+ sourceTable,
548
+ tag: storage.SaveOperationTag.INSERT,
549
+ after: {
550
+ id: 'test2',
551
+ description: 'test2a'
552
+ },
553
+ afterReplicaId: test_utils.rid('test2')
554
+ });
555
+ const result1 = await writer.flush();
556
+ const checkpoint1 = result1?.flushed_op ?? 0n;
557
+ // Test batch
558
+ // b
559
+ await writer.save({
560
+ sourceTable,
561
+ tag: storage.SaveOperationTag.INSERT,
562
+ after: {
563
+ id: 'test1',
564
+ description: 'test1b'
565
+ },
566
+ afterReplicaId: test_utils.rid('test1')
567
+ });
568
+ await writer.save({
569
+ sourceTable,
570
+ tag: storage.SaveOperationTag.UPDATE,
571
+ before: {
572
+ id: 'test1'
573
+ },
574
+ beforeReplicaId: test_utils.rid('test1'),
575
+ after: {
576
+ id: 'test2',
577
+ description: 'test2b'
578
+ },
579
+ afterReplicaId: test_utils.rid('test2')
580
+ });
581
+ await writer.save({
582
+ sourceTable,
583
+ tag: storage.SaveOperationTag.UPDATE,
584
+ before: {
585
+ id: 'test2'
586
+ },
587
+ beforeReplicaId: test_utils.rid('test2'),
588
+ after: {
589
+ id: 'test3',
590
+ description: 'test3b'
591
+ },
592
+ afterReplicaId: test_utils.rid('test3')
593
+ });
594
+ // c
595
+ await writer.save({
596
+ sourceTable,
597
+ tag: storage.SaveOperationTag.UPDATE,
598
+ after: {
599
+ id: 'test2',
600
+ description: 'test2c'
601
+ },
602
+ afterReplicaId: test_utils.rid('test2')
603
+ });
604
+ // d
605
+ await writer.save({
606
+ sourceTable,
607
+ tag: storage.SaveOperationTag.INSERT,
608
+ after: {
609
+ id: 'test4',
610
+ description: 'test4d'
611
+ },
612
+ afterReplicaId: test_utils.rid('test4')
613
+ });
614
+ await writer.save({
615
+ sourceTable,
616
+ tag: storage.SaveOperationTag.UPDATE,
617
+ before: {
618
+ id: 'test4'
619
+ },
620
+ beforeReplicaId: test_utils.rid('test4'),
621
+ after: {
622
+ id: 'test5',
623
+ description: 'test5d'
624
+ },
625
+ afterReplicaId: test_utils.rid('test5')
626
+ });
627
+ const result2 = await writer.flush();
628
+ const checkpoint2 = result2.flushed_op;
629
+ const request = bucketRequest(syncRules, 'global[]', checkpoint1);
630
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint2, [request]));
631
+ const data = batch[0].chunkData.data.map((d) => {
632
+ return {
633
+ op: d.op,
634
+ object_id: d.object_id,
635
+ data: normalizeOplogData(d.data)
636
+ };
637
+ });
638
+ // Operations must be in this order
639
+ expect(data).toEqual([
640
+ // b
641
+ { op: 'PUT', object_id: 'test1', data: JSON.stringify({ id: 'test1', description: 'test1b' }) },
642
+ { op: 'REMOVE', object_id: 'test1', data: null },
643
+ { op: 'PUT', object_id: 'test2', data: JSON.stringify({ id: 'test2', description: 'test2b' }) },
644
+ { op: 'REMOVE', object_id: 'test2', data: null },
645
+ { op: 'PUT', object_id: 'test3', data: JSON.stringify({ id: 'test3', description: 'test3b' }) },
646
+ // c
647
+ { op: 'PUT', object_id: 'test2', data: JSON.stringify({ id: 'test2', description: 'test2c' }) },
648
+ // d
649
+ { op: 'PUT', object_id: 'test4', data: JSON.stringify({ id: 'test4', description: 'test4d' }) },
650
+ { op: 'REMOVE', object_id: 'test4', data: null },
651
+ { op: 'PUT', object_id: 'test5', data: JSON.stringify({ id: 'test5', description: 'test5d' }) }
652
+ ]);
552
653
  });
553
654
  test('changed data with replica identity full', async () => {
554
- const env_6 = { stack: [], error: void 0, hasError: false };
555
- try {
556
- function rid2(id, description) {
557
- return getUuidReplicaIdentityBson({ id, description }, [
558
- { name: 'id', type: 'VARCHAR', typeId: 25 },
559
- { name: 'description', type: 'VARCHAR', typeId: 25 }
560
- ]);
561
- }
562
- const factory = __addDisposableResource(env_6, await generateStorageFactory(), true);
563
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
655
+ function rid2(id, description) {
656
+ return getUuidReplicaIdentityBson({ id, description }, [
657
+ { name: 'id', type: 'VARCHAR', typeId: 25 },
658
+ { name: 'description', type: 'VARCHAR', typeId: 25 }
659
+ ]);
660
+ }
661
+ await using factory = await generateStorageFactory();
662
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
564
663
  bucket_definitions:
565
664
  global:
566
665
  data:
567
666
  - SELECT id, description FROM "test"
568
- `));
569
- const bucketStorage = factory.getInstance(syncRules);
570
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
571
- // 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
- });
582
- });
583
- 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
- });
600
- });
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
- });
613
- });
614
- const checkpoint3 = result3.flushed_op;
615
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint3, bucketRequestMap(syncRules, [['global[]', checkpoint1]])));
616
- const data = batch[0].chunkData.data.map((d) => {
617
- return {
618
- op: d.op,
619
- object_id: d.object_id,
620
- data: normalizeOplogData(d.data),
621
- subkey: d.subkey
622
- };
623
- });
624
- // Operations must be in this order
625
- expect(data).toEqual([
626
- // 2
627
- // The REMOVE is expected because the subkey changes
628
- {
629
- op: 'REMOVE',
630
- object_id: 'test1',
631
- data: null,
632
- subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
633
- },
634
- {
635
- op: 'PUT',
636
- object_id: 'test1',
637
- data: JSON.stringify({ id: 'test1', description: 'test1b' }),
638
- subkey: '6544e3899293153fa7b38331/500e9b68-a2fd-51ff-9c00-313e2fb9f562'
639
- },
640
- // 3
641
- {
642
- op: 'REMOVE',
643
- object_id: 'test1',
644
- data: null,
645
- subkey: '6544e3899293153fa7b38331/500e9b68-a2fd-51ff-9c00-313e2fb9f562'
646
- }
647
- ]);
648
- }
649
- catch (e_6) {
650
- env_6.error = e_6;
651
- env_6.hasError = true;
652
- }
653
- finally {
654
- const result_6 = __disposeResources(env_6);
655
- if (result_6)
656
- await result_6;
657
- }
667
+ `, {
668
+ storageVersion
669
+ }));
670
+ const bucketStorage = factory.getInstance(syncRules);
671
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
672
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
673
+ // Pre-setup
674
+ await writer.markAllSnapshotDone('1/1');
675
+ await writer.save({
676
+ sourceTable,
677
+ tag: storage.SaveOperationTag.INSERT,
678
+ after: {
679
+ id: 'test1',
680
+ description: 'test1a'
681
+ },
682
+ afterReplicaId: rid2('test1', 'test1a')
683
+ });
684
+ const result1 = await writer.flush();
685
+ const checkpoint1 = result1?.flushed_op ?? 0n;
686
+ // Unchanged, but has a before id
687
+ await writer.save({
688
+ sourceTable,
689
+ tag: storage.SaveOperationTag.UPDATE,
690
+ before: {
691
+ id: 'test1',
692
+ description: 'test1a'
693
+ },
694
+ beforeReplicaId: rid2('test1', 'test1a'),
695
+ after: {
696
+ id: 'test1',
697
+ description: 'test1b'
698
+ },
699
+ afterReplicaId: rid2('test1', 'test1b')
700
+ });
701
+ const result2 = await writer.flush();
702
+ // Delete
703
+ await writer.save({
704
+ sourceTable,
705
+ tag: storage.SaveOperationTag.DELETE,
706
+ before: {
707
+ id: 'test1',
708
+ description: 'test1b'
709
+ },
710
+ beforeReplicaId: rid2('test1', 'test1b'),
711
+ after: undefined
712
+ });
713
+ const result3 = await writer.flush();
714
+ const checkpoint3 = result3.flushed_op;
715
+ const request = bucketRequest(syncRules, 'global[]');
716
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint3, [{ ...request, start: checkpoint1 }]));
717
+ const data = batch[0].chunkData.data.map((d) => {
718
+ return {
719
+ op: d.op,
720
+ object_id: d.object_id,
721
+ data: normalizeOplogData(d.data),
722
+ subkey: d.subkey
723
+ };
724
+ });
725
+ // Operations must be in this order
726
+ expect(data).toEqual([
727
+ // 2
728
+ // The REMOVE is expected because the subkey changes
729
+ {
730
+ op: 'REMOVE',
731
+ object_id: 'test1',
732
+ data: null,
733
+ subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
734
+ },
735
+ {
736
+ op: 'PUT',
737
+ object_id: 'test1',
738
+ data: JSON.stringify({ id: 'test1', description: 'test1b' }),
739
+ subkey: '6544e3899293153fa7b38331/500e9b68-a2fd-51ff-9c00-313e2fb9f562'
740
+ },
741
+ // 3
742
+ {
743
+ op: 'REMOVE',
744
+ object_id: 'test1',
745
+ data: null,
746
+ subkey: '6544e3899293153fa7b38331/500e9b68-a2fd-51ff-9c00-313e2fb9f562'
747
+ }
748
+ ]);
658
749
  });
659
750
  test('unchanged data with replica identity full', async () => {
660
- const env_7 = { stack: [], error: void 0, hasError: false };
661
- try {
662
- function rid2(id, description) {
663
- return getUuidReplicaIdentityBson({ id, description }, [
664
- { name: 'id', type: 'VARCHAR', typeId: 25 },
665
- { name: 'description', type: 'VARCHAR', typeId: 25 }
666
- ]);
667
- }
668
- const factory = __addDisposableResource(env_7, await generateStorageFactory(), true);
669
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
751
+ function rid2(id, description) {
752
+ return getUuidReplicaIdentityBson({ id, description }, [
753
+ { name: 'id', type: 'VARCHAR', typeId: 25 },
754
+ { name: 'description', type: 'VARCHAR', typeId: 25 }
755
+ ]);
756
+ }
757
+ await using factory = await generateStorageFactory();
758
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
670
759
  bucket_definitions:
671
760
  global:
672
761
  data:
673
762
  - SELECT id, description FROM "test"
674
- `));
675
- const bucketStorage = factory.getInstance(syncRules);
676
- const sourceTable = test_utils.makeTestTable('test', ['id', 'description']);
677
- // 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
- });
688
- });
689
- 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
- });
706
- });
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
- });
719
- });
720
- const checkpoint3 = result3.flushed_op;
721
- const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint3, bucketRequestMap(syncRules, [['global[]', checkpoint1]])));
722
- const data = batch[0].chunkData.data.map((d) => {
723
- return {
724
- op: d.op,
725
- object_id: d.object_id,
726
- data: normalizeOplogData(d.data),
727
- subkey: d.subkey
728
- };
729
- });
730
- // Operations must be in this order
731
- expect(data).toEqual([
732
- // 2
733
- {
734
- op: 'PUT',
735
- object_id: 'test1',
736
- data: JSON.stringify({ id: 'test1', description: 'test1a' }),
737
- subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
738
- },
739
- // 3
740
- {
741
- op: 'REMOVE',
742
- object_id: 'test1',
743
- data: null,
744
- subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
745
- }
746
- ]);
747
- }
748
- catch (e_7) {
749
- env_7.error = e_7;
750
- env_7.hasError = true;
751
- }
752
- finally {
753
- const result_7 = __disposeResources(env_7);
754
- if (result_7)
755
- await result_7;
756
- }
763
+ `, {
764
+ storageVersion
765
+ }));
766
+ const bucketStorage = factory.getInstance(syncRules);
767
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
768
+ const sourceTable = test_utils.makeTestTable('test', ['id', 'description'], config);
769
+ // Pre-setup
770
+ await writer.markAllSnapshotDone('1/1');
771
+ await writer.save({
772
+ sourceTable,
773
+ tag: storage.SaveOperationTag.INSERT,
774
+ after: {
775
+ id: 'test1',
776
+ description: 'test1a'
777
+ },
778
+ afterReplicaId: rid2('test1', 'test1a')
779
+ });
780
+ const result1 = await writer.flush();
781
+ const checkpoint1 = result1?.flushed_op ?? 0n;
782
+ // Unchanged, but has a before id
783
+ await writer.save({
784
+ sourceTable,
785
+ tag: storage.SaveOperationTag.UPDATE,
786
+ before: {
787
+ id: 'test1',
788
+ description: 'test1a'
789
+ },
790
+ beforeReplicaId: rid2('test1', 'test1a'),
791
+ after: {
792
+ id: 'test1',
793
+ description: 'test1a'
794
+ },
795
+ afterReplicaId: rid2('test1', 'test1a')
796
+ });
797
+ const result2 = await writer.flush();
798
+ // Delete
799
+ await writer.save({
800
+ sourceTable,
801
+ tag: storage.SaveOperationTag.DELETE,
802
+ before: {
803
+ id: 'test1',
804
+ description: 'test1a'
805
+ },
806
+ beforeReplicaId: rid2('test1', 'test1a'),
807
+ after: undefined
808
+ });
809
+ const result3 = await writer.flush();
810
+ const checkpoint3 = result3.flushed_op;
811
+ const request = bucketRequest(syncRules, 'global[]');
812
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint3, [{ ...request, start: checkpoint1 }]));
813
+ const data = batch[0].chunkData.data.map((d) => {
814
+ return {
815
+ op: d.op,
816
+ object_id: d.object_id,
817
+ data: normalizeOplogData(d.data),
818
+ subkey: d.subkey
819
+ };
820
+ });
821
+ // Operations must be in this order
822
+ expect(data).toEqual([
823
+ // 2
824
+ {
825
+ op: 'PUT',
826
+ object_id: 'test1',
827
+ data: JSON.stringify({ id: 'test1', description: 'test1a' }),
828
+ subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
829
+ },
830
+ // 3
831
+ {
832
+ op: 'REMOVE',
833
+ object_id: 'test1',
834
+ data: null,
835
+ subkey: '6544e3899293153fa7b38331/740ba9f2-8b0f-53e3-bb17-5f38a9616f0e'
836
+ }
837
+ ]);
757
838
  });
758
839
  test('large batch', async () => {
759
- const env_8 = { stack: [], error: void 0, hasError: false };
760
- try {
761
- // Test syncing a batch of data that is small in count,
762
- // but large enough in size to be split over multiple returned batches.
763
- // The specific batch splits is an implementation detail of the storage driver,
764
- // and the test will have to updated when other implementations are added.
765
- const factory = __addDisposableResource(env_8, await generateStorageFactory(), true);
766
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
840
+ // Test syncing a batch of data that is small in count,
841
+ // but large enough in size to be split over multiple returned batches.
842
+ // The specific batch splits is an implementation detail of the storage driver,
843
+ // and the test will have to updated when other implementations are added.
844
+ await using factory = await generateStorageFactory();
845
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
767
846
  bucket_definitions:
768
847
  global:
769
848
  data:
770
849
  - SELECT id, description FROM "%"
771
- `));
772
- 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');
814
- });
815
- const { checkpoint } = await bucketStorage.getCheckpoint();
816
- const options = {
817
- chunkLimitBytes: 16 * 1024 * 1024
818
- };
819
- const batch1 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]), options));
820
- expect(test_utils.getBatchData(batch1)).toEqual([
821
- { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
822
- { op_id: '2', op: 'PUT', object_id: 'large1', checksum: 454746904 }
823
- ]);
824
- expect(test_utils.getBatchMeta(batch1)).toEqual({
825
- after: '0',
826
- has_more: true,
827
- next_after: '2'
828
- });
829
- const batch2 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', BigInt(batch1[0].chunkData.next_after)]]), options));
830
- expect(test_utils.getBatchData(batch2)).toEqual([
831
- { op_id: '3', op: 'PUT', object_id: 'large2', checksum: 1795508474 },
832
- { op_id: '4', op: 'PUT', object_id: 'test3', checksum: 1359888332 }
833
- ]);
834
- expect(test_utils.getBatchMeta(batch2)).toEqual({
835
- after: '2',
836
- has_more: false,
837
- next_after: '4'
838
- });
839
- const batch3 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', BigInt(batch2[0].chunkData.next_after)]]), options));
840
- expect(test_utils.getBatchData(batch3)).toEqual([]);
841
- expect(test_utils.getBatchMeta(batch3)).toEqual(null);
842
- }
843
- catch (e_8) {
844
- env_8.error = e_8;
845
- env_8.hasError = true;
846
- }
847
- finally {
848
- const result_8 = __disposeResources(env_8);
849
- if (result_8)
850
- await result_8;
851
- }
850
+ `, {
851
+ storageVersion
852
+ }));
853
+ const bucketStorage = factory.getInstance(syncRules);
854
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
855
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
856
+ await writer.markAllSnapshotDone('1/1');
857
+ const largeDescription = '0123456789'.repeat(12_000_00);
858
+ await writer.save({
859
+ sourceTable,
860
+ tag: storage.SaveOperationTag.INSERT,
861
+ after: {
862
+ id: 'test1',
863
+ description: 'test1'
864
+ },
865
+ afterReplicaId: test_utils.rid('test1')
866
+ });
867
+ await writer.save({
868
+ sourceTable,
869
+ tag: storage.SaveOperationTag.INSERT,
870
+ after: {
871
+ id: 'large1',
872
+ description: largeDescription
873
+ },
874
+ afterReplicaId: test_utils.rid('large1')
875
+ });
876
+ // Large enough to split the returned batch
877
+ await writer.save({
878
+ sourceTable,
879
+ tag: storage.SaveOperationTag.INSERT,
880
+ after: {
881
+ id: 'large2',
882
+ description: largeDescription
883
+ },
884
+ afterReplicaId: test_utils.rid('large2')
885
+ });
886
+ await writer.save({
887
+ sourceTable,
888
+ tag: storage.SaveOperationTag.INSERT,
889
+ after: {
890
+ id: 'test3',
891
+ description: 'test3'
892
+ },
893
+ afterReplicaId: test_utils.rid('test3')
894
+ });
895
+ await writer.commit('1/1');
896
+ const { checkpoint } = await bucketStorage.getCheckpoint();
897
+ const options = {
898
+ chunkLimitBytes: 16 * 1024 * 1024
899
+ };
900
+ const request = bucketRequest(syncRules, 'global[]');
901
+ const batch1 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request], options));
902
+ expect(test_utils.getBatchData(batch1)).toEqual([
903
+ { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
904
+ { op_id: '2', op: 'PUT', object_id: 'large1', checksum: 454746904 }
905
+ ]);
906
+ expect(test_utils.getBatchMeta(batch1)).toEqual({
907
+ after: '0',
908
+ has_more: true,
909
+ next_after: '2'
910
+ });
911
+ const batch2 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch1[0].chunkData.next_after) }], options));
912
+ expect(test_utils.getBatchData(batch2)).toEqual([
913
+ { op_id: '3', op: 'PUT', object_id: 'large2', checksum: 1795508474 },
914
+ { op_id: '4', op: 'PUT', object_id: 'test3', checksum: 1359888332 }
915
+ ]);
916
+ expect(test_utils.getBatchMeta(batch2)).toEqual({
917
+ after: '2',
918
+ has_more: false,
919
+ next_after: '4'
920
+ });
921
+ const batch3 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch2[0].chunkData.next_after) }], options));
922
+ expect(test_utils.getBatchData(batch3)).toEqual([]);
923
+ expect(test_utils.getBatchMeta(batch3)).toEqual(null);
852
924
  });
853
925
  test('long batch', async () => {
854
- const env_9 = { stack: [], error: void 0, hasError: false };
855
- try {
856
- // Test syncing a batch of data that is limited by count.
857
- const factory = __addDisposableResource(env_9, await generateStorageFactory(), true);
858
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
926
+ // Test syncing a batch of data that is limited by count.
927
+ await using factory = await generateStorageFactory();
928
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
859
929
  bucket_definitions:
860
930
  global:
861
931
  data:
862
932
  - SELECT id, description FROM "%"
863
- `));
864
- 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
- });
880
- const { checkpoint } = await bucketStorage.getCheckpoint();
881
- const batch1 = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]), { limit: 4 }));
882
- expect(test_utils.getBatchData(batch1)).toEqual([
883
- { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
884
- { op_id: '2', op: 'PUT', object_id: 'test2', checksum: 730027011 },
885
- { op_id: '3', op: 'PUT', object_id: 'test3', checksum: 1359888332 },
886
- { op_id: '4', op: 'PUT', object_id: 'test4', checksum: 2049153252 }
887
- ]);
888
- expect(test_utils.getBatchMeta(batch1)).toEqual({
889
- after: '0',
890
- has_more: true,
891
- next_after: '4'
892
- });
893
- const batch2 = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', BigInt(batch1.chunkData.next_after)]]), {
894
- limit: 4
895
- }));
896
- expect(test_utils.getBatchData(batch2)).toEqual([
897
- { op_id: '5', op: 'PUT', object_id: 'test5', checksum: 3686902721 },
898
- { op_id: '6', op: 'PUT', object_id: 'test6', checksum: 1974820016 }
899
- ]);
900
- expect(test_utils.getBatchMeta(batch2)).toEqual({
901
- after: '4',
902
- has_more: false,
903
- next_after: '6'
933
+ `, {
934
+ storageVersion
935
+ }));
936
+ const bucketStorage = factory.getInstance(syncRules);
937
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
938
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
939
+ await writer.markAllSnapshotDone('1/1');
940
+ for (let i = 1; i <= 6; i++) {
941
+ await writer.save({
942
+ sourceTable,
943
+ tag: storage.SaveOperationTag.INSERT,
944
+ after: {
945
+ id: `test${i}`,
946
+ description: `test${i}`
947
+ },
948
+ afterReplicaId: `test${i}`
904
949
  });
905
- const batch3 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', BigInt(batch2.chunkData.next_after)]]), {
906
- limit: 4
907
- }));
908
- expect(test_utils.getBatchData(batch3)).toEqual([]);
909
- expect(test_utils.getBatchMeta(batch3)).toEqual(null);
910
- }
911
- catch (e_9) {
912
- env_9.error = e_9;
913
- env_9.hasError = true;
914
- }
915
- finally {
916
- const result_9 = __disposeResources(env_9);
917
- if (result_9)
918
- await result_9;
919
950
  }
951
+ await writer.commit('1/1');
952
+ const { checkpoint } = await bucketStorage.getCheckpoint();
953
+ const request = bucketRequest(syncRules, 'global[]');
954
+ const batch1 = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [request], { limit: 4 }));
955
+ expect(test_utils.getBatchData(batch1)).toEqual([
956
+ { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 },
957
+ { op_id: '2', op: 'PUT', object_id: 'test2', checksum: 730027011 },
958
+ { op_id: '3', op: 'PUT', object_id: 'test3', checksum: 1359888332 },
959
+ { op_id: '4', op: 'PUT', object_id: 'test4', checksum: 2049153252 }
960
+ ]);
961
+ expect(test_utils.getBatchMeta(batch1)).toEqual({
962
+ after: '0',
963
+ has_more: true,
964
+ next_after: '4'
965
+ });
966
+ const batch2 = await test_utils.oneFromAsync(bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch1.chunkData.next_after) }], {
967
+ limit: 4
968
+ }));
969
+ expect(test_utils.getBatchData(batch2)).toEqual([
970
+ { op_id: '5', op: 'PUT', object_id: 'test5', checksum: 3686902721 },
971
+ { op_id: '6', op: 'PUT', object_id: 'test6', checksum: 1974820016 }
972
+ ]);
973
+ expect(test_utils.getBatchMeta(batch2)).toEqual({
974
+ after: '4',
975
+ has_more: false,
976
+ next_after: '6'
977
+ });
978
+ const batch3 = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [{ ...request, start: BigInt(batch2.chunkData.next_after) }], {
979
+ limit: 4
980
+ }));
981
+ expect(test_utils.getBatchData(batch3)).toEqual([]);
982
+ expect(test_utils.getBatchMeta(batch3)).toEqual(null);
920
983
  });
921
984
  describe('batch has_more', () => {
922
985
  const setup = async (options) => {
923
- const env_10 = { stack: [], error: void 0, hasError: false };
924
- try {
925
- const factory = __addDisposableResource(env_10, await generateStorageFactory(), true);
926
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
986
+ await using factory = await generateStorageFactory();
987
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
927
988
  bucket_definitions:
928
989
  global1:
929
990
  data:
@@ -931,46 +992,35 @@ bucket_definitions:
931
992
  global2:
932
993
  data:
933
994
  - SELECT id, description FROM test WHERE bucket = 'global2'
934
- `));
935
- 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');
995
+ `, { storageVersion }));
996
+ const bucketStorage = factory.getInstance(syncRules);
997
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
998
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
999
+ await writer.markAllSnapshotDone('1/1');
1000
+ for (let i = 1; i <= 10; i++) {
1001
+ await writer.save({
1002
+ sourceTable,
1003
+ tag: storage.SaveOperationTag.INSERT,
1004
+ after: {
1005
+ id: `test${i}`,
1006
+ description: `test${i}`,
1007
+ bucket: i == 1 ? 'global1' : 'global2'
1008
+ },
1009
+ afterReplicaId: `test${i}`
951
1010
  });
952
- 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 };
958
- }
959
- catch (e_10) {
960
- env_10.error = e_10;
961
- env_10.hasError = true;
962
- }
963
- finally {
964
- const result_10 = __disposeResources(env_10);
965
- if (result_10)
966
- await result_10;
967
1011
  }
1012
+ await writer.commit('1/1');
1013
+ const { checkpoint } = await bucketStorage.getCheckpoint();
1014
+ const global1Request = bucketRequest(syncRules, 'global1[]', 0n);
1015
+ const global2Request = bucketRequest(syncRules, 'global2[]', 0n);
1016
+ const batch = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(checkpoint, [global1Request, global2Request], options));
1017
+ return { batch, global1Request, global2Request };
968
1018
  };
969
1019
  test('batch has_more (1)', async () => {
970
- const { batch, syncRules } = await setup({ limit: 5 });
1020
+ const { batch, global1Request, global2Request } = await setup({ limit: 5 });
971
1021
  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[]'));
1022
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1023
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
974
1024
  expect(test_utils.getBatchData(batch[0])).toEqual([
975
1025
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
976
1026
  ]);
@@ -992,10 +1042,10 @@ bucket_definitions:
992
1042
  });
993
1043
  });
994
1044
  test('batch has_more (2)', async () => {
995
- const { batch, syncRules } = await setup({ limit: 11 });
1045
+ const { batch, global1Request, global2Request } = await setup({ limit: 11 });
996
1046
  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[]'));
1047
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1048
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
999
1049
  expect(test_utils.getBatchData(batch[0])).toEqual([
1000
1050
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
1001
1051
  ]);
@@ -1023,11 +1073,11 @@ bucket_definitions:
1023
1073
  });
1024
1074
  test('batch has_more (3)', async () => {
1025
1075
  // 50 bytes is more than 1 row, less than 2 rows
1026
- const { batch, syncRules } = await setup({ limit: 3, chunkLimitBytes: 50 });
1076
+ const { batch, global1Request, global2Request } = await setup({ limit: 3, chunkLimitBytes: 50 });
1027
1077
  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[]'));
1078
+ expect(batch[0].chunkData.bucket).toEqual(global1Request.bucket);
1079
+ expect(batch[1].chunkData.bucket).toEqual(global2Request.bucket);
1080
+ expect(batch[2].chunkData.bucket).toEqual(global2Request.bucket);
1031
1081
  expect(test_utils.getBatchData(batch[0])).toEqual([
1032
1082
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
1033
1083
  ]);
@@ -1055,190 +1105,319 @@ bucket_definitions:
1055
1105
  });
1056
1106
  });
1057
1107
  test('empty storage metrics', async () => {
1058
- const env_11 = { stack: [], error: void 0, hasError: false };
1059
- try {
1060
- const f = __addDisposableResource(env_11, await generateStorageFactory({ dropAll: true }), true);
1061
- const metrics = await f.getStorageMetrics();
1062
- expect(metrics).toEqual({
1063
- operations_size_bytes: 0,
1064
- parameters_size_bytes: 0,
1065
- replication_size_bytes: 0
1066
- });
1067
- const r = await f.configureSyncRules(updateSyncRulesFromYaml('bucket_definitions: {}'));
1068
- 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
- });
1072
- await f.getStorageMetrics();
1073
- }
1074
- catch (e_11) {
1075
- env_11.error = e_11;
1076
- env_11.hasError = true;
1077
- }
1078
- finally {
1079
- const result_11 = __disposeResources(env_11);
1080
- if (result_11)
1081
- await result_11;
1082
- }
1108
+ await using f = await generateStorageFactory({ dropAll: true });
1109
+ const metrics = await f.getStorageMetrics();
1110
+ expect(metrics).toEqual({
1111
+ operations_size_bytes: 0,
1112
+ parameters_size_bytes: 0,
1113
+ replication_size_bytes: 0
1114
+ });
1115
+ const r = await f.configureSyncRules(updateSyncRulesFromYaml('bucket_definitions: {}'));
1116
+ const storage = f.getInstance(r.persisted_sync_rules);
1117
+ await using writer = await storage.createWriter(test_utils.BATCH_OPTIONS);
1118
+ await writer.markAllSnapshotDone('1/0');
1119
+ await writer.keepalive('1/0');
1120
+ await f.getStorageMetrics();
1121
+ // We don't care about the specific values here
1083
1122
  });
1084
1123
  test('op_id initialization edge case', async () => {
1085
- const env_12 = { stack: [], error: void 0, hasError: false };
1086
- try {
1087
- // Test syncing a batch of data that is small in count,
1088
- // but large enough in size to be split over multiple returned chunks.
1089
- // Similar to the above test, but splits over 1MB chunks.
1090
- const factory = __addDisposableResource(env_12, await generateStorageFactory(), true);
1091
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1124
+ // Test syncing a batch of data that is small in count,
1125
+ // but large enough in size to be split over multiple returned chunks.
1126
+ // Similar to the above test, but splits over 1MB chunks.
1127
+ await using factory = await generateStorageFactory();
1128
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1092
1129
  bucket_definitions:
1093
1130
  global:
1094
1131
  data:
1095
1132
  - SELECT id FROM test
1096
1133
  - SELECT id FROM test_ignore WHERE false
1097
- `));
1098
- 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
- });
1112
- });
1113
- 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
- });
1123
- });
1124
- const checkpoint2 = result2.flushed_op;
1125
- // we expect 0n and 1n, or 1n and 2n.
1126
- expect(checkpoint2).toBeGreaterThan(checkpoint1);
1127
- }
1128
- catch (e_12) {
1129
- env_12.error = e_12;
1130
- env_12.hasError = true;
1131
- }
1132
- finally {
1133
- const result_12 = __disposeResources(env_12);
1134
- if (result_12)
1135
- await result_12;
1136
- }
1134
+ `, {
1135
+ storageVersion
1136
+ }));
1137
+ const bucketStorage = factory.getInstance(syncRules);
1138
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1139
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config, 1);
1140
+ const sourceTableIgnore = await test_utils.resolveTestTable(writer, 'test_ignore', ['id'], config, 2);
1141
+ await writer.markAllSnapshotDone('1/1');
1142
+ // This saves a record to current_data, but not bucket_data.
1143
+ // This causes a checkpoint to be created without increasing the op_id sequence.
1144
+ await writer.save({
1145
+ sourceTable: sourceTableIgnore,
1146
+ tag: storage.SaveOperationTag.INSERT,
1147
+ after: {
1148
+ id: 'test1'
1149
+ },
1150
+ afterReplicaId: test_utils.rid('test1')
1151
+ });
1152
+ const result1 = await writer.flush();
1153
+ const checkpoint1 = result1.flushed_op;
1154
+ await writer.save({
1155
+ sourceTable: sourceTable,
1156
+ tag: storage.SaveOperationTag.INSERT,
1157
+ after: {
1158
+ id: 'test2'
1159
+ },
1160
+ afterReplicaId: test_utils.rid('test2')
1161
+ });
1162
+ const result2 = await writer.flush();
1163
+ const checkpoint2 = result2.flushed_op;
1164
+ // we expect 0n and 1n, or 1n and 2n.
1165
+ expect(checkpoint2).toBeGreaterThan(checkpoint1);
1137
1166
  });
1138
1167
  test('unchanged checksums', async () => {
1139
- const env_13 = { stack: [], error: void 0, hasError: false };
1140
- try {
1141
- const factory = __addDisposableResource(env_13, await generateStorageFactory(), true);
1142
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1168
+ await using factory = await generateStorageFactory();
1169
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1143
1170
  bucket_definitions:
1144
1171
  global:
1145
1172
  data:
1146
1173
  - SELECT client_id as id, description FROM "%"
1147
- `));
1148
- 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');
1161
- });
1162
- 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 }]);
1171
- }
1172
- catch (e_13) {
1173
- env_13.error = e_13;
1174
- env_13.hasError = true;
1175
- }
1176
- finally {
1177
- const result_13 = __disposeResources(env_13);
1178
- if (result_13)
1179
- await result_13;
1180
- }
1174
+ `, {
1175
+ storageVersion
1176
+ }));
1177
+ const bucketStorage = factory.getInstance(syncRules);
1178
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1179
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1180
+ await writer.markAllSnapshotDone('1/1');
1181
+ await writer.save({
1182
+ sourceTable,
1183
+ tag: storage.SaveOperationTag.INSERT,
1184
+ after: {
1185
+ id: 'test1',
1186
+ description: 'test1a'
1187
+ },
1188
+ afterReplicaId: test_utils.rid('test1')
1189
+ });
1190
+ await writer.commit('1/1');
1191
+ const { checkpoint } = await bucketStorage.getCheckpoint();
1192
+ const request = bucketRequest(syncRules, 'global[]');
1193
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, [request])).values()];
1194
+ expect(checksums).toEqual([{ bucket: request.bucket, checksum: 1917136889, count: 1 }]);
1195
+ const checksums2 = [...(await bucketStorage.getChecksums(checkpoint + 1n, [request])).values()];
1196
+ expect(checksums2).toEqual([{ bucket: request.bucket, checksum: 1917136889, count: 1 }]);
1197
+ });
1198
+ testChecksumBatching(config);
1199
+ test('empty checkpoints (1)', async () => {
1200
+ await using factory = await generateStorageFactory();
1201
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1202
+ bucket_definitions:
1203
+ global:
1204
+ data:
1205
+ - SELECT id, description FROM "%"
1206
+ `, { storageVersion }));
1207
+ const bucketStorage = factory.getInstance(syncRules);
1208
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1209
+ await writer.markAllSnapshotDone('1/1');
1210
+ await writer.commit('1/1');
1211
+ const cp1 = await bucketStorage.getCheckpoint();
1212
+ expect(cp1.lsn).toEqual('1/1');
1213
+ await writer.commit('2/1', { createEmptyCheckpoints: true });
1214
+ const cp2 = await bucketStorage.getCheckpoint();
1215
+ expect(cp2.lsn).toEqual('2/1');
1216
+ await writer.keepalive('3/1');
1217
+ const cp3 = await bucketStorage.getCheckpoint();
1218
+ expect(cp3.lsn).toEqual('3/1');
1219
+ // For the last one, we skip creating empty checkpoints
1220
+ // This means the LSN stays at 3/1.
1221
+ await writer.commit('4/1', { createEmptyCheckpoints: false });
1222
+ const cp4 = await bucketStorage.getCheckpoint();
1223
+ expect(cp4.lsn).toEqual('3/1');
1224
+ });
1225
+ test('empty checkpoints (2)', async () => {
1226
+ await using factory = await generateStorageFactory();
1227
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1228
+ bucket_definitions:
1229
+ global:
1230
+ data:
1231
+ - SELECT id, description FROM "%"
1232
+ `, {
1233
+ storageVersion
1234
+ }));
1235
+ const bucketStorage = factory.getInstance(syncRules);
1236
+ await using writer1 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1237
+ await using writer2 = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1238
+ const sourceTable = await test_utils.resolveTestTable(writer2, 'test', ['id'], config);
1239
+ // We simulate two concurrent batches, but sequential calls are enough for this test.
1240
+ await writer1.markAllSnapshotDone('1/1');
1241
+ await writer1.commit('1/1');
1242
+ await writer1.commit('2/1', { createEmptyCheckpoints: false });
1243
+ const cp2 = await bucketStorage.getCheckpoint();
1244
+ expect(cp2.lsn).toEqual('1/1'); // checkpoint 2/1 skipped
1245
+ await writer2.save({
1246
+ sourceTable,
1247
+ tag: storage.SaveOperationTag.INSERT,
1248
+ after: {
1249
+ id: 'test1',
1250
+ description: 'test1a'
1251
+ },
1252
+ afterReplicaId: test_utils.rid('test1')
1253
+ });
1254
+ // This simulates what happens on a snapshot processor.
1255
+ // This may later change to a flush() rather than commit().
1256
+ await writer2.commit(test_utils.BATCH_OPTIONS.zeroLSN);
1257
+ const cp3 = await bucketStorage.getCheckpoint();
1258
+ expect(cp3.lsn).toEqual('1/1'); // Still unchanged
1259
+ // This now needs to advance the LSN, despite {createEmptyCheckpoints: false}
1260
+ await writer1.commit('4/1', { createEmptyCheckpoints: false });
1261
+ const cp4 = await bucketStorage.getCheckpoint();
1262
+ expect(cp4.lsn).toEqual('4/1');
1263
+ });
1264
+ test('empty checkpoints (sync rule activation)', async () => {
1265
+ await using factory = await generateStorageFactory();
1266
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1267
+ bucket_definitions:
1268
+ global:
1269
+ data:
1270
+ - SELECT id, description FROM "%"
1271
+ `, {
1272
+ storageVersion
1273
+ }));
1274
+ const bucketStorage = factory.getInstance(syncRules);
1275
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1276
+ const result1 = await writer.commit('1/1', { createEmptyCheckpoints: false });
1277
+ expect(result1).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1278
+ // Snapshot is only valid once we reach 3/1
1279
+ await writer.markAllSnapshotDone('3/1');
1280
+ // 2/1 < 3/1 - snapshot not valid yet, block checkpoint
1281
+ const result2 = await writer.commit('2/1', { createEmptyCheckpoints: false });
1282
+ expect(result2).toEqual({ checkpointBlocked: true, checkpointCreated: false });
1283
+ // No empty checkpoint should be created by the commit above.
1284
+ const cp1 = await bucketStorage.getCheckpoint();
1285
+ expect(cp1.lsn).toEqual(null);
1286
+ // After this commit, the snapshot should be valid.
1287
+ // We specifically check that this is done even if createEmptyCheckpoints: false.
1288
+ const result3 = await writer.commit('3/1', { createEmptyCheckpoints: false });
1289
+ expect(result3).toEqual({ checkpointBlocked: false, checkpointCreated: true });
1290
+ // Now, the checkpoint should advance the sync rules active.
1291
+ const cp2 = await bucketStorage.getCheckpoint();
1292
+ expect(cp2.lsn).toEqual('3/1');
1293
+ const activeSyncRules = await factory.getActiveSyncRulesContent();
1294
+ expect(activeSyncRules?.id).toEqual(syncRules.id);
1295
+ // At this point, it should be a truely empty checkpoint
1296
+ const result4 = await writer.commit('4/1', { createEmptyCheckpoints: false });
1297
+ expect(result4).toEqual({ checkpointBlocked: false, checkpointCreated: false });
1298
+ // Unchanged
1299
+ const cp3 = await bucketStorage.getCheckpoint();
1300
+ expect(cp3.lsn).toEqual('3/1');
1301
+ });
1302
+ test.runIf(storageVersion >= 3)('deleting while streaming', async () => {
1303
+ await using factory = await generateStorageFactory();
1304
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1305
+ bucket_definitions:
1306
+ global:
1307
+ data:
1308
+ - SELECT id, description FROM "%"
1309
+ `, {
1310
+ storageVersion
1311
+ }));
1312
+ const bucketStorage = factory.getInstance(syncRules);
1313
+ await using snapshotWriter = await bucketStorage.createWriter({
1314
+ ...test_utils.BATCH_OPTIONS,
1315
+ skipExistingRows: true
1316
+ });
1317
+ await using streamingWriter = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1318
+ const snapshotTable = await test_utils.resolveTestTable(snapshotWriter, 'test', ['id'], config, 1);
1319
+ const streamingTable = await test_utils.resolveTestTable(streamingWriter, 'test', ['id'], config, 1);
1320
+ // We simulate two concurrent batches; separate writers are enough for this test.
1321
+ // For this test, we assume that we start with a row "test1", which is picked up by a snapshot
1322
+ // query, right before the delete is streamed. But the snapshot query is only persisted _after_
1323
+ // the delete is streamed, and we need to ensure that the streamed delete takes precedence.
1324
+ await streamingWriter.save({
1325
+ sourceTable: streamingTable,
1326
+ tag: storage.SaveOperationTag.DELETE,
1327
+ before: {
1328
+ id: 'test1'
1329
+ },
1330
+ beforeReplicaId: test_utils.rid('test1')
1331
+ });
1332
+ await streamingWriter.commit('2/1');
1333
+ await snapshotWriter.save({
1334
+ sourceTable: snapshotTable,
1335
+ tag: storage.SaveOperationTag.INSERT,
1336
+ after: {
1337
+ id: 'test1',
1338
+ description: 'test1a'
1339
+ },
1340
+ afterReplicaId: test_utils.rid('test1')
1341
+ });
1342
+ await snapshotWriter.markAllSnapshotDone('3/1');
1343
+ await snapshotWriter.commit('1/1');
1344
+ await streamingWriter.keepalive('3/1');
1345
+ const cp = await bucketStorage.getCheckpoint();
1346
+ expect(cp.lsn).toEqual('3/1');
1347
+ const data = await test_utils.fromAsync(bucketStorage.getBucketDataBatch(cp.checkpoint, [bucketRequest(syncRules, 'global[]')]));
1348
+ expect(data).toEqual([]);
1181
1349
  });
1182
- testChecksumBatching(generateStorageFactory);
1183
1350
  }
1184
1351
  /**
1185
1352
  * This specifically tests an issue we ran into with MongoDB storage.
1186
1353
  *
1187
1354
  * Exposed as a separate test so we can test with more storage parameters.
1188
1355
  */
1189
- export function testChecksumBatching(generateStorageFactory) {
1356
+ export function testChecksumBatching(config) {
1357
+ const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
1190
1358
  test('checksums for multiple buckets', async () => {
1191
- const env_14 = { stack: [], error: void 0, hasError: false };
1192
- try {
1193
- const factory = __addDisposableResource(env_14, await generateStorageFactory(), true);
1194
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1359
+ await using factory = await config.factory();
1360
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1195
1361
  bucket_definitions:
1196
1362
  user:
1197
1363
  parameters: select request.user_id() as user_id
1198
1364
  data:
1199
1365
  - select id, description from test where user_id = bucket.user_id
1200
- `));
1201
- 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
- }
1218
- }
1219
- await batch.commit('1/1');
1220
- });
1221
- const { checkpoint } = await bucketStorage.getCheckpoint();
1222
- bucketStorage.clearChecksumCache();
1223
- const buckets = bucketRequests(syncRules, ['user["u1"]', 'user["u2"]', 'user["u3"]', 'user["u4"]']);
1224
- const checksums = [...(await bucketStorage.getChecksums(checkpoint, buckets)).values()];
1225
- 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
- ]);
1232
- }
1233
- catch (e_14) {
1234
- env_14.error = e_14;
1235
- env_14.hasError = true;
1236
- }
1237
- finally {
1238
- const result_14 = __disposeResources(env_14);
1239
- if (result_14)
1240
- await result_14;
1366
+ `, {
1367
+ storageVersion
1368
+ }));
1369
+ const bucketStorage = factory.getInstance(syncRules);
1370
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
1371
+ const sourceTable = await test_utils.resolveTestTable(writer, 'test', ['id'], config);
1372
+ await writer.markAllSnapshotDone('1/1');
1373
+ for (let u of ['u1', 'u2', 'u3', 'u4']) {
1374
+ for (let t of ['t1', 't2', 't3', 't4']) {
1375
+ const id = `${t}_${u}`;
1376
+ await writer.save({
1377
+ sourceTable,
1378
+ tag: storage.SaveOperationTag.INSERT,
1379
+ after: {
1380
+ id,
1381
+ description: `${t} description`,
1382
+ user_id: u
1383
+ },
1384
+ afterReplicaId: test_utils.rid(id)
1385
+ });
1386
+ }
1241
1387
  }
1388
+ await writer.commit('1/1');
1389
+ const { checkpoint } = await bucketStorage.getCheckpoint();
1390
+ bucketStorage.clearChecksumCache();
1391
+ const users = ['u1', 'u2', 'u3', 'u4'];
1392
+ const expectedChecksums = [346204588, 5261081, 134760718, -302639724];
1393
+ const bucketRequests = users.map((user) => bucketRequest(syncRules, `user["${user}"]`));
1394
+ const checksums = [...(await bucketStorage.getChecksums(checkpoint, bucketRequests)).values()];
1395
+ checksums.sort((a, b) => a.bucket.localeCompare(b.bucket));
1396
+ const expected = bucketRequests.map((request, index) => ({
1397
+ bucket: request.bucket,
1398
+ count: 4,
1399
+ checksum: expectedChecksums[index]
1400
+ }));
1401
+ expected.sort((a, b) => a.bucket.localeCompare(b.bucket));
1402
+ expect(checksums).toEqual(expected);
1403
+ });
1404
+ test('persists validation errors for sync plan', async () => {
1405
+ await using factory = await config.factory();
1406
+ const deployed = await factory.updateSyncRules(updateSyncRulesFromYaml(`
1407
+ config:
1408
+ edition: 3
1409
+
1410
+ streams:
1411
+ invalid:
1412
+ query: UPDATE test SET foo = 'bar' RETURNING *
1413
+ `));
1414
+ const { errors } = deployed.parsed({ defaultSchema: 'ignored' }).sync_rules;
1415
+ expect(errors).toHaveLength(1);
1416
+ expect(errors[0].message).toStrictEqual('Expected a SELECT statement');
1417
+ expect(errors[0].location).toStrictEqual({
1418
+ start: 54,
1419
+ end: 93
1420
+ });
1242
1421
  });
1243
1422
  }
1244
1423
  //# sourceMappingURL=register-data-storage-data-tests.js.map