@powersync/service-module-mssql 0.4.0 → 0.6.0
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 +45 -0
- package/dist/common/CaptureInstance.d.ts +14 -0
- package/dist/common/CaptureInstance.js +2 -0
- package/dist/common/CaptureInstance.js.map +1 -0
- package/dist/common/MSSQLSourceTable.d.ts +16 -14
- package/dist/common/MSSQLSourceTable.js +35 -16
- package/dist/common/MSSQLSourceTable.js.map +1 -1
- package/dist/replication/CDCPoller.d.ts +42 -20
- package/dist/replication/CDCPoller.js +200 -60
- package/dist/replication/CDCPoller.js.map +1 -1
- package/dist/replication/CDCReplicationJob.js +9 -1
- package/dist/replication/CDCReplicationJob.js.map +1 -1
- package/dist/replication/CDCStream.d.ts +35 -4
- package/dist/replication/CDCStream.js +188 -77
- package/dist/replication/CDCStream.js.map +1 -1
- package/dist/replication/MSSQLConnectionManager.js +16 -5
- package/dist/replication/MSSQLConnectionManager.js.map +1 -1
- package/dist/types/types.d.ts +4 -56
- package/dist/types/types.js +5 -24
- package/dist/types/types.js.map +1 -1
- package/dist/utils/deadlock.d.ts +9 -0
- package/dist/utils/deadlock.js +40 -0
- package/dist/utils/deadlock.js.map +1 -0
- package/dist/utils/mssql.d.ts +33 -15
- package/dist/utils/mssql.js +101 -99
- package/dist/utils/mssql.js.map +1 -1
- package/dist/utils/schema.d.ts +9 -0
- package/dist/utils/schema.js +34 -0
- package/dist/utils/schema.js.map +1 -1
- package/package.json +8 -8
- package/src/common/CaptureInstance.ts +15 -0
- package/src/common/MSSQLSourceTable.ts +33 -24
- package/src/replication/CDCPoller.ts +272 -72
- package/src/replication/CDCReplicationJob.ts +8 -1
- package/src/replication/CDCStream.ts +245 -96
- package/src/replication/MSSQLConnectionManager.ts +15 -5
- package/src/types/types.ts +5 -28
- package/src/utils/deadlock.ts +44 -0
- package/src/utils/mssql.ts +159 -124
- package/src/utils/schema.ts +43 -0
- package/test/src/CDCStream.test.ts +3 -1
- package/test/src/CDCStreamTestContext.ts +28 -7
- package/test/src/CDCStream_resumable_snapshot.test.ts +9 -7
- package/test/src/env.ts +1 -1
- package/test/src/mssql-to-sqlite.test.ts +18 -10
- package/test/src/schema-changes.test.ts +470 -0
- package/test/src/util.ts +84 -15
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,19 +6,24 @@ import {
|
|
|
6
6
|
TimeValuePrecision
|
|
7
7
|
} from '@powersync/service-sync-rules';
|
|
8
8
|
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
clearTestDb,
|
|
11
|
+
createUpperCaseUUID,
|
|
12
|
+
enableCDCForTable,
|
|
13
|
+
TEST_CONNECTION_OPTIONS,
|
|
14
|
+
waitForPendingCDCChanges
|
|
15
|
+
} from './util.js';
|
|
10
16
|
import { CDCToSqliteRow, toSqliteInputRow } from '@module/common/mssqls-to-sqlite.js';
|
|
11
17
|
import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
|
|
12
18
|
import {
|
|
13
|
-
enableCDCForTable,
|
|
14
19
|
escapeIdentifier,
|
|
15
|
-
|
|
20
|
+
getCaptureInstances,
|
|
16
21
|
getLatestLSN,
|
|
17
22
|
getLatestReplicatedLSN,
|
|
18
|
-
getMinLSN,
|
|
19
23
|
toQualifiedTableName
|
|
20
24
|
} from '@module/utils/mssql.js';
|
|
21
25
|
import sql from 'mssql';
|
|
26
|
+
import { CDC_SCHEMA } from '@module/common/MSSQLSourceTable.js';
|
|
22
27
|
|
|
23
28
|
describe('MSSQL Data Types Tests', () => {
|
|
24
29
|
const connectionManager = new MSSQLConnectionManager(TEST_CONNECTION_OPTIONS, {});
|
|
@@ -482,20 +487,23 @@ async function getReplicatedRows(
|
|
|
482
487
|
): Promise<SqliteInputRow[]> {
|
|
483
488
|
const endLSN = await getLatestReplicatedLSN(connectionManager);
|
|
484
489
|
|
|
485
|
-
const
|
|
490
|
+
const captureInstances = await getCaptureInstances({
|
|
486
491
|
connectionManager,
|
|
487
|
-
|
|
488
|
-
|
|
492
|
+
table: {
|
|
493
|
+
schema: connectionManager.schema,
|
|
494
|
+
name: tableName
|
|
495
|
+
}
|
|
489
496
|
});
|
|
490
|
-
if (
|
|
497
|
+
if (captureInstances.size === 0) {
|
|
491
498
|
throw new Error(`No CDC capture instance found for table ${tableName}`);
|
|
492
499
|
}
|
|
493
500
|
|
|
494
|
-
const
|
|
501
|
+
const captureInstance = Array.from(captureInstances.values())[0].instances[0];
|
|
502
|
+
const startLSN = captureInstance.minLSN;
|
|
495
503
|
// Query CDC changes
|
|
496
504
|
const { recordset: results } = await connectionManager.query(
|
|
497
505
|
`
|
|
498
|
-
SELECT * FROM ${
|
|
506
|
+
SELECT * FROM ${CDC_SCHEMA}.fn_cdc_get_all_changes_${captureInstance.name}(@from_lsn, @to_lsn, 'all update old') ORDER BY __$start_lsn, __$seqval
|
|
499
507
|
`,
|
|
500
508
|
[
|
|
501
509
|
{ name: 'from_lsn', type: sql.VarBinary, value: startLSN.toBinary() },
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { putOp, removeOp } from '@powersync/service-core-tests';
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
3
|
+
import { storage } from '@powersync/service-core';
|
|
4
|
+
import sql from 'mssql';
|
|
5
|
+
|
|
6
|
+
import { CDCStreamTestContext } from './CDCStreamTestContext.js';
|
|
7
|
+
import {
|
|
8
|
+
createTestTableWithBasicId,
|
|
9
|
+
describeWithStorage,
|
|
10
|
+
disableCDCForTable,
|
|
11
|
+
dropTestTable,
|
|
12
|
+
enableCDCForTable,
|
|
13
|
+
insertBasicIdTestData,
|
|
14
|
+
renameTable,
|
|
15
|
+
waitForPendingCDCChanges
|
|
16
|
+
} from './util.js';
|
|
17
|
+
import { getLatestLSN, toQualifiedTableName } from '@module/utils/mssql.js';
|
|
18
|
+
import { SchemaChangeType } from '@module/replication/CDCPoller.js';
|
|
19
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
20
|
+
|
|
21
|
+
describe('MSSQL Schema Changes Tests', () => {
|
|
22
|
+
describeWithStorage({ timeout: 60_000 }, defineSchemaChangesTests);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const BASIC_SYNC_RULES = `
|
|
26
|
+
bucket_definitions:
|
|
27
|
+
global:
|
|
28
|
+
data:
|
|
29
|
+
- SELECT id, description FROM "test_data"
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
function defineSchemaChangesTests(config: storage.TestStorageConfig) {
|
|
33
|
+
const { factory } = config;
|
|
34
|
+
|
|
35
|
+
test('Create table: New table in the sync rules', async () => {
|
|
36
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
37
|
+
const { connectionManager } = context;
|
|
38
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
39
|
+
|
|
40
|
+
await context.replicateSnapshot();
|
|
41
|
+
await context.startStreaming();
|
|
42
|
+
|
|
43
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
44
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
45
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
46
|
+
|
|
47
|
+
const data = await context.getFinalBucketState('global[]');
|
|
48
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('Create table: New table created while PowerSync is stopped', async () => {
|
|
52
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
53
|
+
const { connectionManager } = context;
|
|
54
|
+
await context.updateSyncRules(`
|
|
55
|
+
bucket_definitions:
|
|
56
|
+
global:
|
|
57
|
+
data:
|
|
58
|
+
- SELECT id, description FROM "test_data%"
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
await createTestTableWithBasicId(connectionManager, 'test_data1');
|
|
62
|
+
const testData = await insertBasicIdTestData(connectionManager, 'test_data1');
|
|
63
|
+
|
|
64
|
+
await context.replicateSnapshot();
|
|
65
|
+
await context.startStreaming();
|
|
66
|
+
|
|
67
|
+
await context.dispose();
|
|
68
|
+
|
|
69
|
+
await createTestTableWithBasicId(connectionManager, 'test_data2');
|
|
70
|
+
|
|
71
|
+
await using newContext = await CDCStreamTestContext.open(factory, { doNotClear: true });
|
|
72
|
+
await newContext.loadActiveSyncRules();
|
|
73
|
+
|
|
74
|
+
await newContext.replicateSnapshot();
|
|
75
|
+
await newContext.startStreaming();
|
|
76
|
+
|
|
77
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data2');
|
|
78
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data2');
|
|
79
|
+
|
|
80
|
+
const finalState = await newContext.getFinalBucketState('global[]');
|
|
81
|
+
expect(finalState).toMatchObject([
|
|
82
|
+
putOp('test_data1', testData),
|
|
83
|
+
putOp('test_data2', testData1),
|
|
84
|
+
putOp('test_data2', testData2)
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('Create table: New table not in the sync rules', async () => {
|
|
89
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
90
|
+
const { connectionManager } = context;
|
|
91
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
92
|
+
|
|
93
|
+
await context.replicateSnapshot();
|
|
94
|
+
await context.startStreaming();
|
|
95
|
+
|
|
96
|
+
await createTestTableWithBasicId(connectionManager, 'test_data_ignored');
|
|
97
|
+
await insertBasicIdTestData(connectionManager, 'test_data_ignored');
|
|
98
|
+
|
|
99
|
+
const data = await context.getBucketData('global[]');
|
|
100
|
+
expect(data).toMatchObject([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('Drop table: Table in the sync rules', async () => {
|
|
104
|
+
await using context = await CDCStreamTestContext.open(factory, {
|
|
105
|
+
cdcStreamOptions: { schemaCheckIntervalMs: 5000 }
|
|
106
|
+
});
|
|
107
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
108
|
+
|
|
109
|
+
const { connectionManager } = context;
|
|
110
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
111
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
112
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
113
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
114
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
115
|
+
|
|
116
|
+
await context.replicateSnapshot();
|
|
117
|
+
await context.startStreaming();
|
|
118
|
+
|
|
119
|
+
let data = await context.getBucketData('global[]');
|
|
120
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
121
|
+
await dropTestTable(connectionManager, 'test_data');
|
|
122
|
+
|
|
123
|
+
data = await context.getFinalBucketState('global[]');
|
|
124
|
+
expect(data).toMatchObject([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('Re-create table', async () => {
|
|
128
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
129
|
+
const { connectionManager } = context;
|
|
130
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
131
|
+
|
|
132
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
133
|
+
|
|
134
|
+
await context.replicateSnapshot();
|
|
135
|
+
await context.startStreaming();
|
|
136
|
+
|
|
137
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
138
|
+
let data = await context.getBucketData('global[]');
|
|
139
|
+
expect(data).toMatchObject([putOp('test_data', testData1)]);
|
|
140
|
+
|
|
141
|
+
let schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
142
|
+
await dropTestTable(connectionManager, 'test_data');
|
|
143
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.TABLE_DROP);
|
|
144
|
+
|
|
145
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
146
|
+
|
|
147
|
+
const testData = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
148
|
+
|
|
149
|
+
data = await context.getFinalBucketState('global[]');
|
|
150
|
+
expect(data).toMatchObject([putOp('test_data', testData)]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('Rename table: Table not in the sync rules to one in the sync rules', async () => {
|
|
154
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
155
|
+
const { connectionManager } = context;
|
|
156
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
157
|
+
|
|
158
|
+
await createTestTableWithBasicId(connectionManager, 'test_data_old');
|
|
159
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
160
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data_old');
|
|
161
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
162
|
+
|
|
163
|
+
await context.replicateSnapshot();
|
|
164
|
+
await context.startStreaming();
|
|
165
|
+
|
|
166
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
167
|
+
await renameTable(connectionManager, 'test_data_old', 'test_data');
|
|
168
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.TABLE_CREATE);
|
|
169
|
+
|
|
170
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
171
|
+
const data = await context.getFinalBucketState('global[]');
|
|
172
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Rename table: Table in the sync rules to another table in the sync rules', async () => {
|
|
176
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
177
|
+
const { connectionManager } = context;
|
|
178
|
+
|
|
179
|
+
await context.updateSyncRules(`
|
|
180
|
+
bucket_definitions:
|
|
181
|
+
global:
|
|
182
|
+
data:
|
|
183
|
+
- SELECT id, description FROM "test_data%"
|
|
184
|
+
`);
|
|
185
|
+
|
|
186
|
+
await createTestTableWithBasicId(connectionManager, 'test_data1');
|
|
187
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
188
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data1');
|
|
189
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
190
|
+
|
|
191
|
+
await context.replicateSnapshot();
|
|
192
|
+
await context.startStreaming();
|
|
193
|
+
|
|
194
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
195
|
+
await renameTable(connectionManager, 'test_data1', 'test_data2');
|
|
196
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.TABLE_RENAME);
|
|
197
|
+
|
|
198
|
+
const data = await context.getBucketData('global[]');
|
|
199
|
+
expect(data.slice(0, 2)).toMatchObject([
|
|
200
|
+
// Initial replication
|
|
201
|
+
putOp('test_data1', testData1),
|
|
202
|
+
// Initial truncate
|
|
203
|
+
removeOp('test_data1', testData1.id)
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
const finalState = await context.getFinalBucketState('global[]');
|
|
207
|
+
expect(finalState).toMatchObject([putOp('test_data2', testData1)]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('Rename table: Table renamed while PowerSync is stopped', async () => {
|
|
211
|
+
let context = await CDCStreamTestContext.open(factory);
|
|
212
|
+
let { connectionManager } = context;
|
|
213
|
+
|
|
214
|
+
await context.updateSyncRules(`
|
|
215
|
+
bucket_definitions:
|
|
216
|
+
global:
|
|
217
|
+
data:
|
|
218
|
+
- SELECT id, description FROM "test_data%"
|
|
219
|
+
`);
|
|
220
|
+
|
|
221
|
+
await createTestTableWithBasicId(connectionManager, 'test_data1');
|
|
222
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
223
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data1');
|
|
224
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
225
|
+
|
|
226
|
+
await context.replicateSnapshot();
|
|
227
|
+
await context.startStreaming();
|
|
228
|
+
|
|
229
|
+
let data = await context.getBucketData('global[]');
|
|
230
|
+
expect(data).toMatchObject([putOp('test_data1', testData1)]);
|
|
231
|
+
|
|
232
|
+
await context.dispose();
|
|
233
|
+
await renameTable(connectionManager, 'test_data1', 'test_data2');
|
|
234
|
+
|
|
235
|
+
await using newContext = await CDCStreamTestContext.open(factory, { doNotClear: true });
|
|
236
|
+
await newContext.loadActiveSyncRules();
|
|
237
|
+
|
|
238
|
+
await newContext.replicateSnapshot();
|
|
239
|
+
await newContext.startStreaming();
|
|
240
|
+
|
|
241
|
+
const finalState = await newContext.getFinalBucketState('global[]');
|
|
242
|
+
expect(finalState).toMatchObject([putOp('test_data2', testData1)]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('Rename table: Table in the sync rules to not in the sync rules', async () => {
|
|
246
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
247
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
248
|
+
|
|
249
|
+
const { connectionManager } = context;
|
|
250
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
251
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
252
|
+
const testData = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
253
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
254
|
+
|
|
255
|
+
await context.replicateSnapshot();
|
|
256
|
+
await context.startStreaming();
|
|
257
|
+
|
|
258
|
+
let data = await context.getBucketData('global[]');
|
|
259
|
+
expect(data).toMatchObject([putOp('test_data', testData)]);
|
|
260
|
+
|
|
261
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
262
|
+
await renameTable(connectionManager, 'test_data', 'test_data_ignored');
|
|
263
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.TABLE_RENAME);
|
|
264
|
+
|
|
265
|
+
data = await context.getBucketData('global[]');
|
|
266
|
+
expect(data).toMatchObject([
|
|
267
|
+
// Initial replication
|
|
268
|
+
putOp('test_data', testData),
|
|
269
|
+
// Truncate
|
|
270
|
+
removeOp('test_data', testData.id)
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('New capture instance created for replicating table triggers re-snapshot', async () => {
|
|
275
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
276
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
277
|
+
|
|
278
|
+
const { connectionManager } = context;
|
|
279
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
280
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
281
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
282
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
283
|
+
|
|
284
|
+
await context.replicateSnapshot();
|
|
285
|
+
await context.startStreaming();
|
|
286
|
+
|
|
287
|
+
await enableCDCForTable({ connectionManager, table: 'test_data', captureInstance: 'capture_instance_new' });
|
|
288
|
+
|
|
289
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
290
|
+
|
|
291
|
+
const data = await context.getFinalBucketState('global[]');
|
|
292
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('New capture instance created for replicating table while PowerSync is stopped', async () => {
|
|
296
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
297
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
298
|
+
const { connectionManager } = context;
|
|
299
|
+
|
|
300
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
301
|
+
let beforeLSN = await getLatestLSN(connectionManager);
|
|
302
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
303
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
304
|
+
|
|
305
|
+
await context.replicateSnapshot();
|
|
306
|
+
await context.startStreaming();
|
|
307
|
+
|
|
308
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
309
|
+
let data = await context.getBucketData('global[]');
|
|
310
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
311
|
+
|
|
312
|
+
await context.dispose();
|
|
313
|
+
await enableCDCForTable({ connectionManager, table: 'test_data', captureInstance: 'capture_instance_new' });
|
|
314
|
+
|
|
315
|
+
await using newContext = await CDCStreamTestContext.open(factory, { doNotClear: true });
|
|
316
|
+
await newContext.loadActiveSyncRules();
|
|
317
|
+
|
|
318
|
+
await newContext.replicateSnapshot();
|
|
319
|
+
await newContext.startStreaming();
|
|
320
|
+
|
|
321
|
+
const testData3 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
322
|
+
|
|
323
|
+
const finalState = await newContext.getFinalBucketState('global[]');
|
|
324
|
+
expect(finalState).toMatchObject([
|
|
325
|
+
putOp('test_data', testData1),
|
|
326
|
+
putOp('test_data', testData2),
|
|
327
|
+
putOp('test_data', testData3)
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('Capture instance created for a sync rule table without a capture instance', async () => {
|
|
332
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
333
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
334
|
+
const { connectionManager } = context;
|
|
335
|
+
|
|
336
|
+
await createTestTableWithBasicId(connectionManager, 'test_data', false);
|
|
337
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
338
|
+
|
|
339
|
+
await context.replicateSnapshot();
|
|
340
|
+
await context.startStreaming();
|
|
341
|
+
|
|
342
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
343
|
+
await enableCDCForTable({ connectionManager, table: 'test_data' });
|
|
344
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.NEW_CAPTURE_INSTANCE);
|
|
345
|
+
|
|
346
|
+
let data = await context.getBucketData('global[]');
|
|
347
|
+
expect(data).toMatchObject([putOp('test_data', testData1)]);
|
|
348
|
+
|
|
349
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
350
|
+
|
|
351
|
+
data = await context.getFinalBucketState('global[]');
|
|
352
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('Capture instance removed for an actively replicating table', async () => {
|
|
356
|
+
await using context = await CDCStreamTestContext.open(factory, {
|
|
357
|
+
cdcStreamOptions: { schemaCheckIntervalMs: 5000 }
|
|
358
|
+
});
|
|
359
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
360
|
+
const { connectionManager } = context;
|
|
361
|
+
|
|
362
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
363
|
+
let beforeLSN = await getLatestLSN(connectionManager);
|
|
364
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
365
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
366
|
+
|
|
367
|
+
await context.replicateSnapshot();
|
|
368
|
+
await context.startStreaming();
|
|
369
|
+
|
|
370
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
371
|
+
let data = await context.getBucketData('global[]');
|
|
372
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
373
|
+
|
|
374
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
375
|
+
await disableCDCForTable(connectionManager, 'test_data');
|
|
376
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.MISSING_CAPTURE_INSTANCE);
|
|
377
|
+
|
|
378
|
+
data = await context.getBucketData('global[]');
|
|
379
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('Capture instance removed, and then re-added', async () => {
|
|
383
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
384
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
385
|
+
const { connectionManager } = context;
|
|
386
|
+
|
|
387
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
388
|
+
|
|
389
|
+
await context.replicateSnapshot();
|
|
390
|
+
await context.startStreaming();
|
|
391
|
+
|
|
392
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
393
|
+
const testData2 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
394
|
+
let data = await context.getBucketData('global[]');
|
|
395
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
396
|
+
|
|
397
|
+
let schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
398
|
+
await disableCDCForTable(connectionManager, 'test_data');
|
|
399
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.MISSING_CAPTURE_INSTANCE);
|
|
400
|
+
|
|
401
|
+
schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
402
|
+
await enableCDCForTable({ connectionManager, table: 'test_data' });
|
|
403
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.NEW_CAPTURE_INSTANCE);
|
|
404
|
+
|
|
405
|
+
const testData3 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
406
|
+
const testData4 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
407
|
+
|
|
408
|
+
const finalState = await context.getFinalBucketState('global[]');
|
|
409
|
+
expect(finalState).toMatchObject([
|
|
410
|
+
putOp('test_data', testData1),
|
|
411
|
+
putOp('test_data', testData2),
|
|
412
|
+
putOp('test_data', testData3),
|
|
413
|
+
putOp('test_data', testData4)
|
|
414
|
+
]);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('Column schema changes continue replication, but with warning.', async () => {
|
|
418
|
+
await using context = await CDCStreamTestContext.open(factory);
|
|
419
|
+
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
420
|
+
const { connectionManager } = context;
|
|
421
|
+
|
|
422
|
+
await createTestTableWithBasicId(connectionManager, 'test_data');
|
|
423
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
424
|
+
const testData1 = await insertBasicIdTestData(connectionManager, 'test_data');
|
|
425
|
+
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
426
|
+
|
|
427
|
+
await context.replicateSnapshot();
|
|
428
|
+
await context.startStreaming();
|
|
429
|
+
const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
|
|
430
|
+
await connectionManager.query(`ALTER TABLE test_data ADD new_column INT`);
|
|
431
|
+
await expectedSchemaChange(schemaSpy, SchemaChangeType.TABLE_COLUMN_CHANGES);
|
|
432
|
+
|
|
433
|
+
const { recordset: result } = await connectionManager.query(
|
|
434
|
+
`
|
|
435
|
+
INSERT INTO ${toQualifiedTableName(connectionManager.schema, 'test_data')} (description, new_column)
|
|
436
|
+
OUTPUT INSERTED.id, INSERTED.description
|
|
437
|
+
VALUES (@description, @new_column)
|
|
438
|
+
`,
|
|
439
|
+
[
|
|
440
|
+
{ name: 'description', type: sql.NVarChar(sql.MAX), value: 'new_column_description' },
|
|
441
|
+
{ name: 'new_column', type: sql.Int, value: 1 }
|
|
442
|
+
]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const testData2 = { id: result[0].id, description: result[0].description };
|
|
446
|
+
|
|
447
|
+
const data = await context.getBucketData('global[]');
|
|
448
|
+
// Capture instances do not reflect most schema changes until the capture instance is re-created
|
|
449
|
+
// So testData2 will be replicated but without the new column
|
|
450
|
+
expect(data).toMatchObject([putOp('test_data', testData1), putOp('test_data', testData2)]);
|
|
451
|
+
|
|
452
|
+
expect(
|
|
453
|
+
context.cdcStream.tableCache
|
|
454
|
+
.getAll()
|
|
455
|
+
.every((t) => t.captureInstance && t.captureInstance.pendingSchemaChanges.length > 0)
|
|
456
|
+
).toBe(true);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function expectedSchemaChange(spy: any, type: SchemaChangeType) {
|
|
461
|
+
logger.info(`Test Assertion: Waiting for schema change: ${type}`);
|
|
462
|
+
await vi.waitFor(() => expect(spy).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ type })), {
|
|
463
|
+
timeout: 20000
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const promises = spy.mock.results.filter((r: any) => r.type === 'return').map((r: any) => r.value);
|
|
467
|
+
|
|
468
|
+
await Promise.all(promises.map((p: Promise<unknown>) => expect(p).resolves.toBeUndefined()));
|
|
469
|
+
logger.info(`Test Assertion: Received expected schema change: ${type}`);
|
|
470
|
+
}
|