@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.
@@ -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 { createTestTable, describeWithStorage, insertTestData, waitForPendingCDCChanges } from './util.js';
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
58
+ const startRowCount = await getRowsReplicated();
34
59
 
35
60
  await context.replicateSnapshot();
36
61
  await context.startStreaming();
37
62
 
38
- const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
53
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
63
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
160
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
169
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
191
- const startTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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 = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
200
- const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
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
- await this.snapshotPromise;
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
- await this.cdcStream.initReplication();
159
+ this.snapshotPromise = this.cdcStream.initReplication();
160
+ await this.snapshotPromise;
158
161
  this.replicationDone = true;
159
162
  }
160
163