@powersync/service-module-mssql 0.5.0 → 0.6.1
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 +32 -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 +181 -74
- 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 +237 -90
- 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/CDCStreamTestContext.ts +9 -2
- 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 +75 -12
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DatabaseQueryError,
|
|
3
|
+
ErrorCode,
|
|
4
|
+
Logger,
|
|
5
|
+
logger as defaultLogger,
|
|
6
|
+
ReplicationAssertionError
|
|
7
|
+
} from '@powersync/lib-services-framework';
|
|
2
8
|
import timers from 'timers/promises';
|
|
3
9
|
import { MSSQLConnectionManager } from './MSSQLConnectionManager.js';
|
|
4
10
|
import { MSSQLSourceTable } from '../common/MSSQLSourceTable.js';
|
|
5
11
|
import { LSN } from '../common/LSN.js';
|
|
6
12
|
import sql from 'mssql';
|
|
7
|
-
import {
|
|
13
|
+
import { CaptureInstanceDetails, getCaptureInstances, incrementLSN, toQualifiedTableName } from '../utils/mssql.js';
|
|
14
|
+
import { isDeadlockError } from '../utils/deadlock.js';
|
|
8
15
|
import { AdditionalConfig } from '../types/types.js';
|
|
16
|
+
import { tableExists } from '../utils/schema.js';
|
|
17
|
+
import { TablePattern } from '@powersync/service-sync-rules';
|
|
18
|
+
import { CaptureInstance } from '../common/CaptureInstance.js';
|
|
19
|
+
import { SourceEntityDescriptor } from '@powersync/service-core';
|
|
9
20
|
|
|
10
21
|
enum Operation {
|
|
11
22
|
DELETE = 1,
|
|
@@ -13,47 +24,58 @@ enum Operation {
|
|
|
13
24
|
UPDATE_BEFORE = 3,
|
|
14
25
|
UPDATE_AFTER = 4
|
|
15
26
|
}
|
|
16
|
-
|
|
17
|
-
* Schema changes that are detectable by inspecting query events.
|
|
18
|
-
* Create table statements are not included here, since new tables are automatically detected when row events
|
|
19
|
-
* are received for them.
|
|
20
|
-
*/
|
|
27
|
+
|
|
21
28
|
export enum SchemaChangeType {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
TABLE_RENAME = 'table_rename',
|
|
30
|
+
TABLE_DROP = 'table_drop',
|
|
31
|
+
TABLE_CREATE = 'table_create',
|
|
32
|
+
TABLE_COLUMN_CHANGES = 'table_column_changes',
|
|
33
|
+
NEW_CAPTURE_INSTANCE = 'new_capture_instance',
|
|
34
|
+
MISSING_CAPTURE_INSTANCE = 'missing_capture_instance'
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export interface SchemaChange {
|
|
30
38
|
type: SchemaChangeType;
|
|
31
39
|
/**
|
|
32
|
-
* The table that the schema change applies to.
|
|
40
|
+
* The table that the schema change applies to. Populated for table drops, renames, new capture instances, and DDL changes.
|
|
33
41
|
*/
|
|
34
|
-
table
|
|
35
|
-
schema: string;
|
|
42
|
+
table?: MSSQLSourceTable;
|
|
36
43
|
/**
|
|
37
|
-
* Populated for
|
|
44
|
+
* Populated for new tables or renames, but only if the new table matches a sync rule source table.
|
|
38
45
|
*/
|
|
39
|
-
newTable?:
|
|
46
|
+
newTable?: Omit<SourceEntityDescriptor, 'replicaIdColumns'>;
|
|
47
|
+
|
|
48
|
+
newCaptureInstance?: CaptureInstance;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export interface CDCEventHandler {
|
|
43
|
-
onInsert: (row: any, table: MSSQLSourceTable,
|
|
44
|
-
onUpdate: (rowAfter: any, rowBefore: any, table: MSSQLSourceTable,
|
|
45
|
-
onDelete: (row: any, table: MSSQLSourceTable,
|
|
52
|
+
onInsert: (row: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => Promise<void>;
|
|
53
|
+
onUpdate: (rowAfter: any, rowBefore: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => Promise<void>;
|
|
54
|
+
onDelete: (row: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => Promise<void>;
|
|
46
55
|
onCommit: (lsn: string, transactionCount: number) => Promise<void>;
|
|
47
56
|
onSchemaChange: (change: SchemaChange) => Promise<void>;
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
export const DEFAULT_SCHEMA_CHECK_INTERVAL_MS = 60_000;
|
|
60
|
+
|
|
50
61
|
export interface CDCPollerOptions {
|
|
51
62
|
connectionManager: MSSQLConnectionManager;
|
|
52
63
|
eventHandler: CDCEventHandler;
|
|
53
|
-
|
|
64
|
+
/** CDC enabled source tables from the sync rules to replicate */
|
|
65
|
+
getReplicatedTables: () => MSSQLSourceTable[];
|
|
66
|
+
/** All table patterns from the sync rules. Can contain tables that need to be replicated
|
|
67
|
+
* but do not yet have CDC enabled
|
|
68
|
+
*/
|
|
69
|
+
sourceTables: TablePattern[];
|
|
54
70
|
startLSN: LSN;
|
|
55
71
|
logger?: Logger;
|
|
56
72
|
additionalConfig: AdditionalConfig;
|
|
73
|
+
/**
|
|
74
|
+
* Interval in milliseconds between schema change checks.
|
|
75
|
+
* Schema checks also run immediately after a recoverable error during polling
|
|
76
|
+
* (e.g. a dropped capture instance).
|
|
77
|
+
*/
|
|
78
|
+
schemaCheckIntervalMs?: number;
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
/**
|
|
@@ -65,10 +87,12 @@ export class CDCPoller {
|
|
|
65
87
|
private currentLSN: LSN;
|
|
66
88
|
private logger: Logger;
|
|
67
89
|
private listenerError: Error | null;
|
|
90
|
+
private captureInstances: Map<number, CaptureInstanceDetails>;
|
|
68
91
|
|
|
69
92
|
private isStopped: boolean = false;
|
|
70
93
|
private isStopping: boolean = false;
|
|
71
94
|
private isPolling: boolean = false;
|
|
95
|
+
private lastSchemaCheckTime: number = 0;
|
|
72
96
|
|
|
73
97
|
constructor(public options: CDCPollerOptions) {
|
|
74
98
|
this.logger = options.logger ?? defaultLogger;
|
|
@@ -76,6 +100,7 @@ export class CDCPoller {
|
|
|
76
100
|
this.eventHandler = options.eventHandler;
|
|
77
101
|
this.currentLSN = options.startLSN;
|
|
78
102
|
this.listenerError = null;
|
|
103
|
+
this.captureInstances = new Map<number, CaptureInstanceDetails>();
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
private get pollingBatchSize(): number {
|
|
@@ -86,8 +111,12 @@ export class CDCPoller {
|
|
|
86
111
|
return this.options.additionalConfig.pollingIntervalMs;
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
private get
|
|
90
|
-
return this.options.
|
|
114
|
+
private get schemaCheckIntervalMs(): number {
|
|
115
|
+
return this.options.schemaCheckIntervalMs ?? DEFAULT_SCHEMA_CHECK_INTERVAL_MS;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private get replicatedTables(): MSSQLSourceTable[] {
|
|
119
|
+
return this.options.getReplicatedTables();
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
public async stop(): Promise<void> {
|
|
@@ -107,14 +136,45 @@ export class CDCPoller {
|
|
|
107
136
|
}
|
|
108
137
|
|
|
109
138
|
try {
|
|
139
|
+
if (this.shouldCheckSchema()) {
|
|
140
|
+
this.captureInstances = await getCaptureInstances({ connectionManager: this.connectionManager });
|
|
141
|
+
const schemaChanges = await this.checkForSchemaChanges();
|
|
142
|
+
for (const schemaChange of schemaChanges) {
|
|
143
|
+
await this.eventHandler.onSchemaChange(schemaChange);
|
|
144
|
+
}
|
|
145
|
+
this.lastSchemaCheckTime = Date.now();
|
|
146
|
+
|
|
147
|
+
this.logger.debug(
|
|
148
|
+
`Schema change check complete. Schema changes found: ${schemaChanges.map((c) => c.type).join(', ')}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
110
152
|
const hasChanges = await this.poll();
|
|
111
153
|
if (!hasChanges) {
|
|
112
|
-
// No changes found, wait before
|
|
154
|
+
// No changes found, wait before polling again
|
|
113
155
|
await timers.setTimeout(this.pollingIntervalMs);
|
|
114
156
|
}
|
|
157
|
+
|
|
115
158
|
// If changes were found, poll immediately again (no wait)
|
|
116
159
|
} catch (error) {
|
|
117
160
|
if (!(this.isStopped || this.isStopping)) {
|
|
161
|
+
// Recoverable errors
|
|
162
|
+
if (error instanceof DatabaseQueryError) {
|
|
163
|
+
this.logger.warn(error.message);
|
|
164
|
+
// Force schema check on next iteration to detect breaking changes
|
|
165
|
+
this.lastSchemaCheckTime = 0;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Deadlock errors are transient — even if all retries within retryOnDeadlock were
|
|
169
|
+
// exhausted, we should not crash the poller. Instead, log and retry the entire cycle.
|
|
170
|
+
if (isDeadlockError(error)) {
|
|
171
|
+
this.logger.warn(
|
|
172
|
+
`Deadlock persisted after all retry attempts during CDC polling cycle. Will retry on next cycle: ${(error as Error).message}`
|
|
173
|
+
);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Non-recoverable errors
|
|
118
178
|
this.listenerError = error as Error;
|
|
119
179
|
this.logger.error('Error during CDC polling:', error);
|
|
120
180
|
this.stop();
|
|
@@ -160,17 +220,22 @@ export class CDCPoller {
|
|
|
160
220
|
this.logger.info(`Polling bounds are ${startLSN} -> ${endLSN} spanning ${results.length} transaction(s).`);
|
|
161
221
|
|
|
162
222
|
let transactionCount = 0;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
223
|
+
this.logger.debug(
|
|
224
|
+
`Currently replicating tables: ${this.replicatedTables.map((table) => table.toQualifiedName()).join(', ')}`
|
|
225
|
+
);
|
|
226
|
+
for (const table of this.replicatedTables) {
|
|
227
|
+
if (table.enabledForCDC()) {
|
|
228
|
+
const tableTransactionCount = await this.pollTable(table, { startLSN, endLSN });
|
|
229
|
+
// We poll for batch size transactions, but these include transactions not applicable to our Source Tables.
|
|
230
|
+
// Each Source Table may or may not have transactions that are applicable to it, so just keep track of the highest number of transactions processed for any Source Table.
|
|
231
|
+
if (tableTransactionCount > transactionCount) {
|
|
232
|
+
transactionCount = tableTransactionCount;
|
|
233
|
+
}
|
|
169
234
|
}
|
|
170
235
|
}
|
|
171
236
|
|
|
172
237
|
this.logger.info(
|
|
173
|
-
`Processed ${results.length} transaction(s), including ${transactionCount} Source Table transaction(s)
|
|
238
|
+
`Processed ${results.length} transaction(s), including ${transactionCount} Source Table transaction(s). Commited LSN: ${endLSN.toString()}`
|
|
174
239
|
);
|
|
175
240
|
// Call eventHandler.onCommit() with toLSN after processing all tables
|
|
176
241
|
await this.eventHandler.onCommit(endLSN.toString(), transactionCount);
|
|
@@ -186,61 +251,196 @@ export class CDCPoller {
|
|
|
186
251
|
|
|
187
252
|
private async pollTable(table: MSSQLSourceTable, bounds: { startLSN: LSN; endLSN: LSN }): Promise<number> {
|
|
188
253
|
// Ensure that the startLSN is not before the minimum LSN for the table
|
|
189
|
-
const minLSN =
|
|
254
|
+
const minLSN = this.captureInstances.get(table.objectId)!.instances[0].minLSN;
|
|
190
255
|
if (minLSN > bounds.endLSN) {
|
|
191
256
|
return 0;
|
|
192
257
|
} else if (minLSN >= bounds.startLSN) {
|
|
193
258
|
bounds.startLSN = minLSN;
|
|
194
259
|
}
|
|
195
|
-
|
|
196
|
-
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const { recordset: results } = await this.connectionManager.query(
|
|
263
|
+
`
|
|
197
264
|
SELECT * FROM ${table.allChangesFunction}(@from_lsn, @to_lsn, 'all update old') ORDER BY __$start_lsn, __$seqval
|
|
198
265
|
`,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
[
|
|
267
|
+
{ name: 'from_lsn', type: sql.VarBinary, value: bounds.startLSN.toBinary() },
|
|
268
|
+
{ name: 'to_lsn', type: sql.VarBinary, value: bounds.endLSN.toBinary() }
|
|
269
|
+
]
|
|
270
|
+
);
|
|
204
271
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
272
|
+
let transactionCount = 0;
|
|
273
|
+
let updateBefore: any = null;
|
|
274
|
+
let lastTransactionLSN: LSN | null = null;
|
|
275
|
+
for (const row of results) {
|
|
276
|
+
const transactionLSN = LSN.fromBinary(row.__$start_lsn);
|
|
277
|
+
switch (row.__$operation) {
|
|
278
|
+
case Operation.DELETE:
|
|
279
|
+
await this.eventHandler.onDelete(row, table, results.columns);
|
|
280
|
+
this.logger.info(`Processed DELETE row LSN: ${transactionLSN}`);
|
|
281
|
+
break;
|
|
282
|
+
case Operation.INSERT:
|
|
283
|
+
await this.eventHandler.onInsert(row, table, results.columns);
|
|
284
|
+
this.logger.info(`Processed INSERT row LSN: ${transactionLSN}`);
|
|
285
|
+
break;
|
|
286
|
+
case Operation.UPDATE_BEFORE:
|
|
287
|
+
updateBefore = row;
|
|
288
|
+
this.logger.debug(`Processed UPDATE, before row LSN: ${transactionLSN}`);
|
|
289
|
+
break;
|
|
290
|
+
case Operation.UPDATE_AFTER:
|
|
291
|
+
if (updateBefore === null) {
|
|
292
|
+
throw new ReplicationAssertionError('Missing before image for update event.');
|
|
293
|
+
}
|
|
294
|
+
await this.eventHandler.onUpdate(row, updateBefore, table, results.columns);
|
|
295
|
+
updateBefore = null;
|
|
296
|
+
this.logger.info(`Processed UPDATE row LSN: ${transactionLSN}`);
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
this.logger.warn(`Unknown operation type [${row.__$operation}] encountered in CDC changes.`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Increment transaction count when we encounter a new transaction LSN (except for UPDATE_BEFORE rows)
|
|
303
|
+
if (transactionLSN != lastTransactionLSN) {
|
|
304
|
+
lastTransactionLSN = transactionLSN;
|
|
305
|
+
if (row.__$operation !== Operation.UPDATE_BEFORE) {
|
|
306
|
+
transactionCount++;
|
|
226
307
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return transactionCount;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
// This Covers both deleted tables and capture instances
|
|
314
|
+
if (error.message.includes(`Invalid object name`)) {
|
|
315
|
+
throw new DatabaseQueryError(
|
|
316
|
+
ErrorCode.PSYNC_S1601,
|
|
317
|
+
`Capture instance for table ${table.toQualifiedName()} is no longer available.`,
|
|
318
|
+
error
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private shouldCheckSchema(): boolean {
|
|
326
|
+
return Date.now() - this.lastSchemaCheckTime >= this.schemaCheckIntervalMs;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Checks the given table for pending schema changes that can lead to inconsistencies in the replicated data if not handled.
|
|
331
|
+
* Returns the SchemaChange if any are found, null otherwise.
|
|
332
|
+
*/
|
|
333
|
+
private async checkForSchemaChanges(): Promise<SchemaChange[]> {
|
|
334
|
+
const schemaChanges: SchemaChange[] = [];
|
|
335
|
+
|
|
336
|
+
const newTables = this.checkForNewTables();
|
|
337
|
+
for (const table of newTables) {
|
|
338
|
+
this.logger.info(
|
|
339
|
+
`New table ${toQualifiedTableName(table.sourceTable.schema, table.sourceTable.name)} matching the sync rules has been created. Handling schema change...`
|
|
340
|
+
);
|
|
341
|
+
schemaChanges.push({
|
|
342
|
+
type: SchemaChangeType.TABLE_CREATE,
|
|
343
|
+
newTable: {
|
|
344
|
+
name: table.sourceTable.name,
|
|
345
|
+
schema: table.sourceTable.schema,
|
|
346
|
+
objectId: table.sourceTable.objectId
|
|
347
|
+
},
|
|
348
|
+
newCaptureInstance: table.instances[0]
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const table of this.replicatedTables) {
|
|
353
|
+
const exists = await tableExists(table.objectId, this.connectionManager);
|
|
354
|
+
if (!exists) {
|
|
355
|
+
this.logger.info(`Table ${table.toQualifiedName()} has been dropped. Handling schema change...`);
|
|
356
|
+
schemaChanges.push({
|
|
357
|
+
type: SchemaChangeType.TABLE_DROP,
|
|
358
|
+
table
|
|
359
|
+
});
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const captureInstanceDetails = this.captureInstances.get(table.objectId);
|
|
364
|
+
if (!captureInstanceDetails) {
|
|
365
|
+
if (table.enabledForCDC()) {
|
|
366
|
+
// Table had a capture instance but no longer does.
|
|
367
|
+
schemaChanges.push({
|
|
368
|
+
type: SchemaChangeType.MISSING_CAPTURE_INSTANCE,
|
|
369
|
+
table
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const latestCaptureInstance = captureInstanceDetails.instances[0];
|
|
376
|
+
// If the table is not enabled for CDC or the capture instance is different, we need to re-snapshot the source table
|
|
377
|
+
if (!table.enabledForCDC() || table.captureInstance!.objectId !== latestCaptureInstance.objectId) {
|
|
378
|
+
schemaChanges.push({
|
|
379
|
+
type: SchemaChangeType.NEW_CAPTURE_INSTANCE,
|
|
380
|
+
table,
|
|
381
|
+
newCaptureInstance: latestCaptureInstance
|
|
382
|
+
});
|
|
383
|
+
continue;
|
|
233
384
|
}
|
|
234
385
|
|
|
235
|
-
//
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
386
|
+
// One of the replicated tables has been renamed
|
|
387
|
+
if (table.sourceTable.name !== captureInstanceDetails.sourceTable.name) {
|
|
388
|
+
const newTable = this.tableMatchesSyncRules(
|
|
389
|
+
captureInstanceDetails.sourceTable.schema,
|
|
390
|
+
captureInstanceDetails.sourceTable.name
|
|
391
|
+
)
|
|
392
|
+
? {
|
|
393
|
+
name: captureInstanceDetails.sourceTable.name,
|
|
394
|
+
schema: captureInstanceDetails.sourceTable.schema,
|
|
395
|
+
objectId: captureInstanceDetails.sourceTable.objectId
|
|
396
|
+
}
|
|
397
|
+
: undefined;
|
|
398
|
+
|
|
399
|
+
schemaChanges.push({
|
|
400
|
+
type: SchemaChangeType.TABLE_RENAME,
|
|
401
|
+
table,
|
|
402
|
+
newTable,
|
|
403
|
+
newCaptureInstance: latestCaptureInstance
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (latestCaptureInstance.pendingSchemaChanges.length > 0) {
|
|
409
|
+
schemaChanges.push({
|
|
410
|
+
type: SchemaChangeType.TABLE_COLUMN_CHANGES,
|
|
411
|
+
table,
|
|
412
|
+
newCaptureInstance: latestCaptureInstance
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return schemaChanges;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private checkForNewTables(): CaptureInstanceDetails[] {
|
|
421
|
+
const newTables: CaptureInstanceDetails[] = [];
|
|
422
|
+
for (const [objectId, captureInstanceDetails] of this.captureInstances.entries()) {
|
|
423
|
+
// If a source table is not in the replicated tables array, but a capture instance exists for it, it is potentially a new table to replicate.
|
|
424
|
+
if (!this.replicatedTables.some((table) => table.objectId === objectId)) {
|
|
425
|
+
// Check if the new table matches any of the sync rules source tables.
|
|
426
|
+
if (
|
|
427
|
+
this.tableMatchesSyncRules(captureInstanceDetails.sourceTable.schema, captureInstanceDetails.sourceTable.name)
|
|
428
|
+
) {
|
|
429
|
+
newTables.push(captureInstanceDetails);
|
|
240
430
|
}
|
|
241
431
|
}
|
|
242
432
|
}
|
|
243
433
|
|
|
244
|
-
return
|
|
434
|
+
return newTables;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private tableMatchesSyncRules(schema: string, tableName: string): boolean {
|
|
438
|
+
return this.options.sourceTables.some((tablePattern) =>
|
|
439
|
+
tablePattern.matches({
|
|
440
|
+
connectionTag: this.connectionManager.connectionTag,
|
|
441
|
+
schema: schema,
|
|
442
|
+
name: tableName
|
|
443
|
+
})
|
|
444
|
+
);
|
|
245
445
|
}
|
|
246
446
|
}
|
|
@@ -3,6 +3,7 @@ import { MSSQLConnectionManagerFactory } from './MSSQLConnectionManagerFactory.j
|
|
|
3
3
|
import { container, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
4
4
|
import { CDCDataExpiredError, CDCStream } from './CDCStream.js';
|
|
5
5
|
import { AdditionalConfig } from '../types/types.js';
|
|
6
|
+
import { POWERSYNC_CHECKPOINTS_TABLE } from '../utils/mssql.js';
|
|
6
7
|
|
|
7
8
|
export interface CDCReplicationJobOptions extends replication.AbstractReplicationJobOptions {
|
|
8
9
|
connectionFactory: MSSQLConnectionManagerFactory;
|
|
@@ -22,7 +23,13 @@ export class CDCReplicationJob extends replication.AbstractReplicationJob {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async keepAlive() {
|
|
25
|
-
|
|
26
|
+
if (this.lastStream) {
|
|
27
|
+
try {
|
|
28
|
+
await this.lastStream.keepAlive();
|
|
29
|
+
} catch (e) {
|
|
30
|
+
this.logger.warn(`KeepAlive failed, unable to write an update to the ${POWERSYNC_CHECKPOINTS_TABLE} table`, e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
async replicate() {
|