@powersync/service-module-mssql 0.10.3 → 0.10.4
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 +44 -0
- package/dist/api/MSSQLRouteAPIAdapter.js +2 -1
- package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
- package/dist/module/MSSQLModule.js +1 -0
- package/dist/module/MSSQLModule.js.map +1 -1
- package/dist/replication/CDCStream.d.ts +2 -4
- package/dist/replication/CDCStream.js +54 -57
- package/dist/replication/CDCStream.js.map +1 -1
- package/dist/replication/MSSQLSnapshotQuery.d.ts +25 -15
- package/dist/replication/MSSQLSnapshotQuery.js +74 -47
- package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
- package/dist/utils/mssql.d.ts +0 -1
- package/dist/utils/mssql.js +0 -19
- package/dist/utils/mssql.js.map +1 -1
- package/package.json +9 -9
- package/src/api/MSSQLRouteAPIAdapter.ts +2 -1
- package/src/module/MSSQLModule.ts +1 -0
- package/src/replication/CDCStream.ts +56 -59
- package/src/replication/MSSQLSnapshotQuery.ts +107 -62
- package/src/utils/mssql.ts +0 -22
- package/test/src/CDCStream.test.ts +106 -16
- package/test/src/CDCStreamTestContext.ts +5 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
|
|
1
2
|
import { getLatestLSN } from '@module/utils/mssql.js';
|
|
3
|
+
import { ReplicationAbortedError } from '@powersync/lib-services-framework';
|
|
2
4
|
import { storage } from '@powersync/service-core';
|
|
3
5
|
import { METRICS_HELPER, putOp, removeOp } from '@powersync/service-core-tests';
|
|
4
6
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
5
7
|
import sql from 'mssql';
|
|
6
|
-
import { describe, expect, test } from 'vitest';
|
|
8
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
7
9
|
import { CDCStreamTestContext } from './CDCStreamTestContext.js';
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createTestTable,
|
|
12
|
+
describeWithStorage,
|
|
13
|
+
enableCDCForTable,
|
|
14
|
+
insertTestData,
|
|
15
|
+
waitForPendingCDCChanges
|
|
16
|
+
} from './util.js';
|
|
9
17
|
|
|
10
18
|
const BASIC_SYNC_RULES = `
|
|
11
19
|
bucket_definitions:
|
|
@@ -14,6 +22,23 @@ bucket_definitions:
|
|
|
14
22
|
- SELECT id, description FROM "test_data"
|
|
15
23
|
`;
|
|
16
24
|
|
|
25
|
+
const COMPOSITE_KEY_SYNC_CONFIG = `
|
|
26
|
+
config:
|
|
27
|
+
edition: 3
|
|
28
|
+
streams:
|
|
29
|
+
global:
|
|
30
|
+
auto_subscribe: true
|
|
31
|
+
query: SELECT id_user AS id, id_user, id_visit FROM test_composite
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
async function getRowsReplicated() {
|
|
35
|
+
return (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getTransactionsReplicated() {
|
|
39
|
+
return (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
describe('CDCStream tests', () => {
|
|
18
43
|
describeWithStorage({ timeout: 20_000 }, defineCDCStreamTests);
|
|
19
44
|
});
|
|
@@ -30,12 +55,12 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
30
55
|
const beforeLSN = await getLatestLSN(connectionManager);
|
|
31
56
|
const testData = await insertTestData(connectionManager, 'test_data');
|
|
32
57
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
33
|
-
const startRowCount =
|
|
58
|
+
const startRowCount = await getRowsReplicated();
|
|
34
59
|
|
|
35
60
|
await context.replicateSnapshot();
|
|
36
61
|
await context.startStreaming();
|
|
37
62
|
|
|
38
|
-
const endRowCount =
|
|
63
|
+
const endRowCount = await getRowsReplicated();
|
|
39
64
|
const data = await context.getBucketData('global[]');
|
|
40
65
|
expect(data).toMatchObject([putOp('test_data', testData)]);
|
|
41
66
|
expect(endRowCount - startRowCount).toEqual(1);
|
|
@@ -49,8 +74,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
49
74
|
await createTestTable(connectionManager, 'test_data');
|
|
50
75
|
await context.replicateSnapshot();
|
|
51
76
|
|
|
52
|
-
const startRowCount =
|
|
53
|
-
const startTxCount =
|
|
77
|
+
const startRowCount = await getRowsReplicated();
|
|
78
|
+
const startTxCount = await getTransactionsReplicated();
|
|
54
79
|
|
|
55
80
|
await context.startStreaming();
|
|
56
81
|
|
|
@@ -59,8 +84,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
59
84
|
const data = await context.getBucketData('global[]');
|
|
60
85
|
|
|
61
86
|
expect(data).toMatchObject([putOp('test_data', testData)]);
|
|
62
|
-
const endRowCount =
|
|
63
|
-
const endTxCount =
|
|
87
|
+
const endRowCount = await getRowsReplicated();
|
|
88
|
+
const endTxCount = await getTransactionsReplicated();
|
|
64
89
|
expect(endRowCount - startRowCount).toEqual(1);
|
|
65
90
|
expect(endTxCount - startTxCount).toEqual(1);
|
|
66
91
|
});
|
|
@@ -156,8 +181,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
156
181
|
|
|
157
182
|
await context.replicateSnapshot();
|
|
158
183
|
|
|
159
|
-
const startRowCount =
|
|
160
|
-
const startTxCount =
|
|
184
|
+
const startRowCount = await getRowsReplicated();
|
|
185
|
+
const startTxCount = await getTransactionsReplicated();
|
|
161
186
|
|
|
162
187
|
await context.startStreaming();
|
|
163
188
|
|
|
@@ -165,8 +190,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
165
190
|
const data = await context.getBucketData('global[]');
|
|
166
191
|
|
|
167
192
|
expect(data).toMatchObject([]);
|
|
168
|
-
const endRowCount =
|
|
169
|
-
const endTxCount =
|
|
193
|
+
const endRowCount = await getRowsReplicated();
|
|
194
|
+
const endTxCount = await getTransactionsReplicated();
|
|
170
195
|
|
|
171
196
|
// There was a transaction, but it is not counted since it is not for a table in the sync config
|
|
172
197
|
expect(endRowCount - startRowCount).toEqual(0);
|
|
@@ -187,8 +212,8 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
187
212
|
|
|
188
213
|
await context.replicateSnapshot();
|
|
189
214
|
|
|
190
|
-
const startRowCount =
|
|
191
|
-
const startTxCount =
|
|
215
|
+
const startRowCount = await getRowsReplicated();
|
|
216
|
+
const startTxCount = await getTransactionsReplicated();
|
|
192
217
|
|
|
193
218
|
await context.startStreaming();
|
|
194
219
|
|
|
@@ -196,9 +221,74 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
|
|
|
196
221
|
const data = await context.getBucketData('global[]');
|
|
197
222
|
|
|
198
223
|
expect(data).toMatchObject([putOp('test_DATA', testData)]);
|
|
199
|
-
const endRowCount =
|
|
200
|
-
const endTxCount =
|
|
224
|
+
const endRowCount = await getRowsReplicated();
|
|
225
|
+
const endTxCount = await getTransactionsReplicated();
|
|
201
226
|
expect(endRowCount - startRowCount).toEqual(1);
|
|
202
227
|
expect(endTxCount - startTxCount).toBeGreaterThanOrEqual(1);
|
|
203
228
|
});
|
|
229
|
+
|
|
230
|
+
// 250 rows end with a partial batch, 300 rows end with an empty batch.
|
|
231
|
+
test.each([250, 300])(
|
|
232
|
+
'Initial snapshot for a SimpleSnapshotQuery table with %i rows, larger than the snapshot batch size.',
|
|
233
|
+
async (rowCount) => {
|
|
234
|
+
await using context = await CDCStreamTestContext.open(factory, { cdcStreamOptions: { snapshotBatchSize: 100 } });
|
|
235
|
+
const { connectionManager } = context;
|
|
236
|
+
await context.updateSyncRules(COMPOSITE_KEY_SYNC_CONFIG);
|
|
237
|
+
|
|
238
|
+
await createCompositeKeyTable(connectionManager);
|
|
239
|
+
await insertCompositeKeyTableRows(connectionManager, rowCount);
|
|
240
|
+
const startRowCount = await getRowsReplicated();
|
|
241
|
+
|
|
242
|
+
await context.replicateSnapshot();
|
|
243
|
+
await context.startStreaming();
|
|
244
|
+
|
|
245
|
+
const endRowCount = await getRowsReplicated();
|
|
246
|
+
expect(endRowCount - startRowCount).toEqual(rowCount);
|
|
247
|
+
expect(await context.getBucketData('global|0[]')).toHaveLength(rowCount);
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
test('Interrupting initial snapshot for a SimpleSnapshotQuery table between batches', async () => {
|
|
252
|
+
// The snapshot query is still in progress when the snapshot is interrupted between batches.
|
|
253
|
+
// It must be cancelled before the snapshot transaction can be rolled back.
|
|
254
|
+
await using context = await CDCStreamTestContext.open(factory, { cdcStreamOptions: { snapshotBatchSize: 100 } });
|
|
255
|
+
const { connectionManager } = context;
|
|
256
|
+
await context.updateSyncRules(COMPOSITE_KEY_SYNC_CONFIG);
|
|
257
|
+
await createCompositeKeyTable(connectionManager);
|
|
258
|
+
await insertCompositeKeyTableRows(connectionManager, 5000);
|
|
259
|
+
const startRowCount = await getRowsReplicated();
|
|
260
|
+
|
|
261
|
+
const snapshotError = context.replicateSnapshot().then(
|
|
262
|
+
() => null,
|
|
263
|
+
(e) => e
|
|
264
|
+
);
|
|
265
|
+
await vi.waitFor(async () => expect((await getRowsReplicated()) - startRowCount).toBeGreaterThanOrEqual(200), {
|
|
266
|
+
timeout: 10_000,
|
|
267
|
+
interval: 1
|
|
268
|
+
});
|
|
269
|
+
await context.dispose();
|
|
270
|
+
|
|
271
|
+
expect(await snapshotError).toBeInstanceOf(ReplicationAbortedError);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A composite primary key cannot use BatchedSnapshotQuery, so this table is snapshotted with SimpleSnapshotQuery.
|
|
277
|
+
*/
|
|
278
|
+
async function createCompositeKeyTable(connectionManager: MSSQLConnectionManager) {
|
|
279
|
+
await connectionManager.query(`
|
|
280
|
+
CREATE TABLE test_composite (
|
|
281
|
+
id_user INT NOT NULL,
|
|
282
|
+
id_visit INT NOT NULL,
|
|
283
|
+
CONSTRAINT PK_test_composite PRIMARY KEY (id_user, id_visit)
|
|
284
|
+
)`);
|
|
285
|
+
await enableCDCForTable({ connectionManager, table: 'test_composite' });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function insertCompositeKeyTableRows(connectionManager: MSSQLConnectionManager, rowCount: number) {
|
|
289
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
290
|
+
await connectionManager.query(`
|
|
291
|
+
WITH n AS (SELECT 1 AS i UNION ALL SELECT i + 1 FROM n WHERE i < ${rowCount})
|
|
292
|
+
INSERT INTO test_composite (id_user, id_visit) SELECT i, i % 10 FROM n OPTION (MAXRECURSION ${rowCount})`);
|
|
293
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
204
294
|
}
|
|
@@ -64,7 +64,9 @@ export class CDCStreamTestContext implements AsyncDisposable {
|
|
|
64
64
|
|
|
65
65
|
async dispose() {
|
|
66
66
|
this.abortController.abort();
|
|
67
|
-
|
|
67
|
+
// Let an interrupted snapshot finish before closing its connections.
|
|
68
|
+
// Its error is handled by the caller of replicateSnapshot().
|
|
69
|
+
await this.snapshotPromise?.catch(() => {});
|
|
68
70
|
await this.streamPromise;
|
|
69
71
|
await this.connectionManager.end();
|
|
70
72
|
await this.factory?.[Symbol.asyncDispose]();
|
|
@@ -154,7 +156,8 @@ export class CDCStreamTestContext implements AsyncDisposable {
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
async replicateSnapshot() {
|
|
157
|
-
|
|
159
|
+
this.snapshotPromise = this.cdcStream.initReplication();
|
|
160
|
+
await this.snapshotPromise;
|
|
158
161
|
this.replicationDone = true;
|
|
159
162
|
}
|
|
160
163
|
|