@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.
- package/CHANGELOG.md +95 -3
- package/dist/test-utils/MetricsHelper.js +1 -1
- package/dist/test-utils/MetricsHelper.js.map +1 -1
- package/dist/test-utils/StorageDataHelpers.d.ts +8 -0
- package/dist/test-utils/StorageDataHelpers.js +33 -0
- package/dist/test-utils/StorageDataHelpers.js.map +1 -0
- package/dist/test-utils/general-utils.d.ts +22 -3
- package/dist/test-utils/general-utils.js +56 -3
- package/dist/test-utils/general-utils.js.map +1 -1
- package/dist/test-utils/stream_utils.js +2 -2
- package/dist/test-utils/stream_utils.js.map +1 -1
- package/dist/test-utils/test-utils-index.d.ts +1 -0
- package/dist/test-utils/test-utils-index.js +1 -0
- package/dist/test-utils/test-utils-index.js.map +1 -1
- package/dist/tests/register-compacting-tests.d.ts +1 -1
- package/dist/tests/register-compacting-tests.js +514 -587
- package/dist/tests/register-compacting-tests.js.map +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.d.ts +1 -1
- package/dist/tests/register-data-storage-checkpoint-tests.js +200 -301
- package/dist/tests/register-data-storage-checkpoint-tests.js.map +1 -1
- package/dist/tests/register-data-storage-data-tests.d.ts +2 -2
- package/dist/tests/register-data-storage-data-tests.js +1220 -1041
- package/dist/tests/register-data-storage-data-tests.js.map +1 -1
- package/dist/tests/register-data-storage-parameter-tests.d.ts +1 -1
- package/dist/tests/register-data-storage-parameter-tests.js +681 -632
- package/dist/tests/register-data-storage-parameter-tests.js.map +1 -1
- package/dist/tests/register-migration-tests.js +63 -139
- package/dist/tests/register-migration-tests.js.map +1 -1
- package/dist/tests/register-parameter-compacting-tests.d.ts +1 -1
- package/dist/tests/register-parameter-compacting-tests.js +121 -201
- package/dist/tests/register-parameter-compacting-tests.js.map +1 -1
- package/dist/tests/register-sync-tests.d.ts +2 -1
- package/dist/tests/register-sync-tests.js +974 -1136
- package/dist/tests/register-sync-tests.js.map +1 -1
- package/dist/tests/tests-index.d.ts +4 -4
- package/dist/tests/tests-index.js +4 -4
- package/dist/tests/tests-index.js.map +1 -1
- package/dist/tests/util.d.ts +5 -4
- package/dist/tests/util.js +27 -12
- package/dist/tests/util.js.map +1 -1
- package/package.json +6 -6
- package/src/test-utils/MetricsHelper.ts +1 -1
- package/src/test-utils/StorageDataHelpers.ts +44 -0
- package/src/test-utils/general-utils.ts +81 -4
- package/src/test-utils/stream_utils.ts +2 -2
- package/src/test-utils/test-utils-index.ts +1 -0
- package/src/tests/register-compacting-tests.ts +376 -322
- package/src/tests/register-data-storage-checkpoint-tests.ts +85 -53
- package/src/tests/register-data-storage-data-tests.ts +1076 -563
- package/src/tests/register-data-storage-parameter-tests.ts +631 -327
- package/src/tests/register-parameter-compacting-tests.ts +92 -96
- package/src/tests/register-sync-tests.ts +463 -379
- package/src/tests/tests-index.ts +4 -4
- package/src/tests/util.ts +46 -17
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,59 +1,7 @@
|
|
|
1
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
},
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
},
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
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
|
-
|
|
924
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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,
|
|
1020
|
+
const { batch, global1Request, global2Request } = await setup({ limit: 5 });
|
|
971
1021
|
expect(batch.length).toEqual(2);
|
|
972
|
-
expect(batch[0].chunkData.bucket).toEqual(
|
|
973
|
-
expect(batch[1].chunkData.bucket).toEqual(
|
|
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,
|
|
1045
|
+
const { batch, global1Request, global2Request } = await setup({ limit: 11 });
|
|
996
1046
|
expect(batch.length).toEqual(2);
|
|
997
|
-
expect(batch[0].chunkData.bucket).toEqual(
|
|
998
|
-
expect(batch[1].chunkData.bucket).toEqual(
|
|
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,
|
|
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(
|
|
1029
|
-
expect(batch[1].chunkData.bucket).toEqual(
|
|
1030
|
-
expect(batch[2].chunkData.bucket).toEqual(
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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(
|
|
1356
|
+
export function testChecksumBatching(config) {
|
|
1357
|
+
const storageVersion = config.storageVersion ?? CURRENT_STORAGE_VERSION;
|
|
1190
1358
|
test('checksums for multiple buckets', async () => {
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|