@powersync/service-module-mssql 0.6.4 → 0.8.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 +48 -0
- package/dist/api/MSSQLRouteAPIAdapter.d.ts +3 -4
- package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/MSSQLSourceTable.d.ts +13 -3
- package/dist/common/MSSQLSourceTable.js +25 -8
- package/dist/common/MSSQLSourceTable.js.map +1 -1
- package/dist/common/MSSQLSourceTableCache.js +1 -1
- package/dist/common/MSSQLSourceTableCache.js.map +1 -1
- package/dist/replication/CDCPoller.d.ts +2 -2
- package/dist/replication/CDCPoller.js +1 -1
- package/dist/replication/CDCPoller.js.map +1 -1
- package/dist/replication/CDCReplicationJob.js +1 -1
- package/dist/replication/CDCReplicationJob.js.map +1 -1
- package/dist/replication/CDCReplicator.js +1 -1
- package/dist/replication/CDCReplicator.js.map +1 -1
- package/dist/replication/CDCStream.js +103 -81
- package/dist/replication/CDCStream.js.map +1 -1
- package/dist/replication/MSSQLSnapshotQuery.d.ts +5 -6
- package/dist/replication/MSSQLSnapshotQuery.js +13 -15
- package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
- package/dist/utils/mssql.js.map +1 -1
- package/dist/utils/schema.d.ts +6 -2
- package/dist/utils/schema.js.map +1 -1
- package/package.json +11 -21
- package/src/api/MSSQLRouteAPIAdapter.ts +3 -4
- package/src/common/MSSQLSourceTable.ts +30 -7
- package/src/common/MSSQLSourceTableCache.ts +1 -1
- package/src/replication/CDCPoller.ts +3 -4
- package/src/replication/CDCReplicationJob.ts +1 -1
- package/src/replication/CDCReplicator.ts +1 -1
- package/src/replication/CDCStream.ts +125 -88
- package/src/replication/MSSQLSnapshotQuery.ts +10 -11
- package/src/utils/mssql.ts +1 -1
- package/src/utils/schema.ts +7 -2
- package/test/src/CDCStreamTestContext.ts +20 -20
- package/test/src/schema-changes.test.ts +0 -2
- package/test/src/util.ts +1 -1
- package/tsconfig.json +0 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
storage
|
|
16
16
|
} from '@powersync/service-core';
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { HydratedSyncConfig, SqliteInputRow, SqliteRow, TablePattern } from '@powersync/service-sync-rules';
|
|
19
19
|
|
|
20
20
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
21
21
|
import sql from 'mssql';
|
|
@@ -34,7 +34,12 @@ import {
|
|
|
34
34
|
getLatestReplicatedLSN,
|
|
35
35
|
isIColumnMetadata
|
|
36
36
|
} from '../utils/mssql.js';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
getReplicationIdentityColumns,
|
|
39
|
+
getTablesFromPattern,
|
|
40
|
+
ResolvedTable,
|
|
41
|
+
SourceTableChangeRef
|
|
42
|
+
} from '../utils/schema.js';
|
|
38
43
|
import { CDCEventHandler, CDCPoller, SchemaChange, SchemaChangeType } from './CDCPoller.js';
|
|
39
44
|
import { MSSQLConnectionManager } from './MSSQLConnectionManager.js';
|
|
40
45
|
import { BatchedSnapshotQuery, MSSQLSnapshotQuery, SimpleSnapshotQuery } from './MSSQLSnapshotQuery.js';
|
|
@@ -99,7 +104,7 @@ export class CDCDataExpiredError extends DatabaseConnectionError {
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
export class CDCStream {
|
|
102
|
-
private readonly syncRules:
|
|
107
|
+
private readonly syncRules: HydratedSyncConfig;
|
|
103
108
|
private readonly storage: storage.SyncRulesBucketStorage;
|
|
104
109
|
private readonly connections: MSSQLConnectionManager;
|
|
105
110
|
private readonly abortSignal: AbortSignal;
|
|
@@ -134,7 +139,7 @@ export class CDCStream {
|
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
get groupId() {
|
|
137
|
-
return this.options.storage.
|
|
142
|
+
return this.options.storage.replicationStreamId;
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
get connectionId() {
|
|
@@ -202,6 +207,16 @@ export class CDCStream {
|
|
|
202
207
|
const tables: MSSQLSourceTable[] = [];
|
|
203
208
|
for (const matchedTable of matchedTables) {
|
|
204
209
|
const captureInstanceDetails = captureInstances.get(matchedTable.objectId as number);
|
|
210
|
+
if (!captureInstanceDetails) {
|
|
211
|
+
// Match Postgres publication handling: If the source table cannot stream changes yet,
|
|
212
|
+
// don't resolve it into storage. Once CDC is enabled, the schema poller detects the
|
|
213
|
+
// new capture instance and resolves/snapshots it then.
|
|
214
|
+
this.logger.info(
|
|
215
|
+
`Skipping ${tablePattern.schema}.${matchedTable.name} - not enabled for CDC. This table will not be replicated until CDC is enabled for it.`
|
|
216
|
+
);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
// TODO: Check RLS settings for table
|
|
206
221
|
|
|
207
222
|
const replicaIdColumns = await getReplicationIdentityColumns({
|
|
@@ -213,12 +228,13 @@ export class CDCStream {
|
|
|
213
228
|
const table = await this.processTable(
|
|
214
229
|
batch,
|
|
215
230
|
{
|
|
231
|
+
connectionTag: this.connectionTag,
|
|
216
232
|
name: matchedTable.name,
|
|
217
233
|
schema: matchedTable.schema,
|
|
218
234
|
objectId: matchedTable.objectId,
|
|
219
235
|
replicaIdColumns: replicaIdColumns.columns
|
|
220
236
|
},
|
|
221
|
-
captureInstanceDetails
|
|
237
|
+
captureInstanceDetails.instances[0],
|
|
222
238
|
false
|
|
223
239
|
);
|
|
224
240
|
|
|
@@ -236,14 +252,11 @@ export class CDCStream {
|
|
|
236
252
|
if (!table.objectId && typeof table.objectId != 'number') {
|
|
237
253
|
throw new ReplicationAssertionError(`objectId expected, got ${typeof table.objectId}`);
|
|
238
254
|
}
|
|
239
|
-
const resolved = await
|
|
240
|
-
group_id: this.groupId,
|
|
255
|
+
const resolved = await batch.resolveTables({
|
|
241
256
|
connection_id: this.connectionId,
|
|
242
|
-
|
|
243
|
-
entity_descriptor: table,
|
|
244
|
-
sync_rules: this.syncRules
|
|
257
|
+
source: table
|
|
245
258
|
});
|
|
246
|
-
const resolvedTable = new MSSQLSourceTable(resolved.
|
|
259
|
+
const resolvedTable = new MSSQLSourceTable(table, resolved.tables);
|
|
247
260
|
|
|
248
261
|
if (!captureInstance) {
|
|
249
262
|
this.logger.warn(
|
|
@@ -261,28 +274,36 @@ export class CDCStream {
|
|
|
261
274
|
// Snapshot if:
|
|
262
275
|
// 1. The table is in the sync config and snapshot is requested, or not already done.
|
|
263
276
|
// 2. AND the table is enabled for CDC with a valid capture instance.
|
|
264
|
-
const
|
|
265
|
-
snapshot && !
|
|
277
|
+
const snapshotCandidates = resolved.tables.filter(
|
|
278
|
+
(candidate) => snapshot && !candidate.snapshotComplete && candidate.syncAny
|
|
279
|
+
);
|
|
280
|
+
const shouldSnapshot = snapshotCandidates.length > 0 && resolvedTable.enabledForCDC();
|
|
266
281
|
|
|
267
282
|
if (shouldSnapshot) {
|
|
268
|
-
// Truncate
|
|
269
|
-
await batch.truncate(
|
|
283
|
+
// Truncate tables in case a previous snapshot was interrupted.
|
|
284
|
+
await batch.truncate(snapshotCandidates);
|
|
270
285
|
|
|
271
|
-
// Start the snapshot inside a transaction.
|
|
272
|
-
|
|
286
|
+
// Start the snapshot(s) inside a transaction.
|
|
287
|
+
for (const candidate of snapshotCandidates) {
|
|
288
|
+
await this.snapshotTableInTx(batch, resolvedTable, candidate);
|
|
289
|
+
}
|
|
273
290
|
}
|
|
274
291
|
|
|
275
292
|
return resolvedTable;
|
|
276
293
|
}
|
|
277
294
|
|
|
278
|
-
private async snapshotTableInTx(
|
|
295
|
+
private async snapshotTableInTx(
|
|
296
|
+
batch: storage.BucketStorageBatch,
|
|
297
|
+
physicalTable: MSSQLSourceTable,
|
|
298
|
+
sourceTable: storage.SourceTable
|
|
299
|
+
): Promise<void> {
|
|
279
300
|
// Note: We use the "Read Committed" isolation level here, not snapshot isolation.
|
|
280
301
|
// The data may change during the transaction, but that is compensated for in the streaming
|
|
281
302
|
// replication afterward.
|
|
282
303
|
const transaction = await this.connections.createTransaction();
|
|
283
304
|
await transaction.begin(sql.ISOLATION_LEVEL.READ_COMMITTED);
|
|
284
305
|
try {
|
|
285
|
-
await this.snapshotTable(batch, transaction,
|
|
306
|
+
await this.snapshotTable(batch, transaction, physicalTable, sourceTable);
|
|
286
307
|
|
|
287
308
|
// Get the current LSN.
|
|
288
309
|
// The data will only be consistent once incremental replication has passed that point.
|
|
@@ -299,9 +320,9 @@ export class CDCStream {
|
|
|
299
320
|
const postSnapshotLSN = await getLatestLSN(this.connections);
|
|
300
321
|
// Side note: A ROLLBACK would probably also be fine here, since we only read in this transaction.
|
|
301
322
|
await transaction.commit();
|
|
302
|
-
const [updatedSourceTable] = await batch.markTableSnapshotDone([
|
|
323
|
+
const [updatedSourceTable] = await batch.markTableSnapshotDone([sourceTable], postSnapshotLSN.toString());
|
|
303
324
|
this.logger.info(
|
|
304
|
-
`Snapshot of ${
|
|
325
|
+
`Snapshot of ${physicalTable.toQualifiedName()} completed. Post-snapshot LSN: ${postSnapshotLSN.toString()}`
|
|
305
326
|
);
|
|
306
327
|
this.tableCache.updateSourceTable(updatedSourceTable);
|
|
307
328
|
} catch (e) {
|
|
@@ -313,39 +334,41 @@ export class CDCStream {
|
|
|
313
334
|
private async snapshotTable(
|
|
314
335
|
batch: storage.BucketStorageBatch,
|
|
315
336
|
transaction: sql.Transaction,
|
|
316
|
-
|
|
337
|
+
physicalTable: MSSQLSourceTable,
|
|
338
|
+
sourceTable: storage.SourceTable
|
|
317
339
|
) {
|
|
318
|
-
let totalEstimatedCount =
|
|
319
|
-
let replicatedCount =
|
|
340
|
+
let totalEstimatedCount = sourceTable.snapshotStatus?.totalEstimatedCount;
|
|
341
|
+
let replicatedCount = sourceTable.snapshotStatus?.replicatedCount ?? 0;
|
|
320
342
|
let lastCountTime = 0;
|
|
321
343
|
let query: MSSQLSnapshotQuery;
|
|
322
344
|
// We do streaming on two levels:
|
|
323
345
|
// 1. Coarse select from the entire table, stream rows 1 by one
|
|
324
346
|
// 2. Fine level: Stream batches of rows with each fetch call
|
|
325
|
-
if (BatchedSnapshotQuery.supports(
|
|
347
|
+
if (BatchedSnapshotQuery.supports(sourceTable)) {
|
|
326
348
|
// Single primary key - we can use the primary key for chunking
|
|
327
|
-
const orderByKey =
|
|
349
|
+
const orderByKey = sourceTable.replicaIdColumns[0];
|
|
328
350
|
query = new BatchedSnapshotQuery(
|
|
329
351
|
transaction,
|
|
330
|
-
|
|
352
|
+
physicalTable.toQualifiedName(),
|
|
353
|
+
sourceTable,
|
|
331
354
|
this.snapshotBatchSize,
|
|
332
|
-
|
|
355
|
+
sourceTable.snapshotStatus?.lastKey ?? null
|
|
333
356
|
);
|
|
334
|
-
if (
|
|
357
|
+
if (sourceTable.snapshotStatus?.lastKey != null) {
|
|
335
358
|
this.logger.info(
|
|
336
|
-
`Snapshotting ${
|
|
359
|
+
`Snapshotting ${physicalTable.toQualifiedName()} ${sourceTable.formatSnapshotProgress()} - resuming from ${orderByKey.name} > ${(query as BatchedSnapshotQuery).lastKey}`
|
|
337
360
|
);
|
|
338
361
|
} else {
|
|
339
362
|
this.logger.info(
|
|
340
|
-
`Snapshotting ${
|
|
363
|
+
`Snapshotting ${physicalTable.toQualifiedName()} ${sourceTable.formatSnapshotProgress()} - resumable`
|
|
341
364
|
);
|
|
342
365
|
}
|
|
343
366
|
} else {
|
|
344
367
|
// Fallback case - query the entire table
|
|
345
368
|
this.logger.info(
|
|
346
|
-
`Snapshotting ${
|
|
369
|
+
`Snapshotting ${physicalTable.toQualifiedName()} ${sourceTable.formatSnapshotProgress()} - not resumable`
|
|
347
370
|
);
|
|
348
|
-
query = new SimpleSnapshotQuery(transaction,
|
|
371
|
+
query = new SimpleSnapshotQuery(transaction, physicalTable.toQualifiedName());
|
|
349
372
|
replicatedCount = 0;
|
|
350
373
|
}
|
|
351
374
|
await query.initialize();
|
|
@@ -372,11 +395,11 @@ export class CDCStream {
|
|
|
372
395
|
// This auto-flushes when the batch reaches its size limit
|
|
373
396
|
await batch.save({
|
|
374
397
|
tag: storage.SaveOperationTag.INSERT,
|
|
375
|
-
sourceTable
|
|
398
|
+
sourceTable,
|
|
376
399
|
before: undefined,
|
|
377
400
|
beforeReplicaId: undefined,
|
|
378
401
|
after: row,
|
|
379
|
-
afterReplicaId: getUuidReplicaIdentityBson(row,
|
|
402
|
+
afterReplicaId: getUuidReplicaIdentityBson(row, sourceTable.replicaIdColumns)
|
|
380
403
|
});
|
|
381
404
|
|
|
382
405
|
replicatedCount++;
|
|
@@ -399,15 +422,16 @@ export class CDCStream {
|
|
|
399
422
|
// the default "Read Committed" isolation level. This means we can get new data
|
|
400
423
|
// within the transaction, so we re-estimate the count every 10 minutes when replicating
|
|
401
424
|
// large tables.
|
|
402
|
-
totalEstimatedCount = await this.estimatedCountNumber(
|
|
425
|
+
totalEstimatedCount = await this.estimatedCountNumber(physicalTable, transaction);
|
|
403
426
|
lastCountTime = performance.now();
|
|
404
427
|
}
|
|
405
|
-
const updatedSourceTable = await batch.updateTableProgress(
|
|
428
|
+
const updatedSourceTable = await batch.updateTableProgress(sourceTable, {
|
|
406
429
|
lastKey: lastKey,
|
|
407
430
|
replicatedCount: replicatedCount,
|
|
408
431
|
totalEstimatedCount: totalEstimatedCount
|
|
409
432
|
});
|
|
410
433
|
this.tableCache.updateSourceTable(updatedSourceTable);
|
|
434
|
+
sourceTable = updatedSourceTable;
|
|
411
435
|
|
|
412
436
|
if (this.abortSignal.aborted) {
|
|
413
437
|
// We only abort after flushing
|
|
@@ -418,7 +442,7 @@ export class CDCStream {
|
|
|
418
442
|
if (batchReplicatedCount < this.snapshotBatchSize) {
|
|
419
443
|
hasRemainingData = false;
|
|
420
444
|
} else {
|
|
421
|
-
this.logger.info(`Snapshotting ${
|
|
445
|
+
this.logger.info(`Snapshotting ${physicalTable.toQualifiedName()} ${sourceTable.formatSnapshotProgress()}`);
|
|
422
446
|
}
|
|
423
447
|
}
|
|
424
448
|
}
|
|
@@ -471,18 +495,19 @@ export class CDCStream {
|
|
|
471
495
|
break;
|
|
472
496
|
case SnapshotStatus.LIMITED_RESNAPSHOT:
|
|
473
497
|
for (const table of specificTablesToResnapshot!) {
|
|
474
|
-
await batch.drop(
|
|
498
|
+
await batch.drop(table.getReplicatedSourceTables());
|
|
475
499
|
// Update table in the table cache
|
|
476
|
-
await this.processTable(batch, table.
|
|
500
|
+
await this.processTable(batch, table.ref, table.captureInstance, false);
|
|
477
501
|
}
|
|
478
502
|
break;
|
|
479
503
|
default:
|
|
480
504
|
throw new ReplicationAssertionError(`Unsupported snapshot status: ${status}`);
|
|
481
505
|
}
|
|
482
506
|
|
|
483
|
-
const tablesToSnapshot: MSSQLSourceTable[] = [];
|
|
507
|
+
const tablesToSnapshot: { physicalTable: MSSQLSourceTable; sourceTable: storage.SourceTable }[] = [];
|
|
484
508
|
for (const table of this.tableCache.getAll()) {
|
|
485
|
-
|
|
509
|
+
const sourceTablesToSnapshot = table.sourceTables.filter((sourceTable) => !sourceTable.snapshotComplete);
|
|
510
|
+
if (sourceTablesToSnapshot.length == 0) {
|
|
486
511
|
this.logger.info(`Skipping table [${table.toQualifiedName()}] - snapshot already done.`);
|
|
487
512
|
continue;
|
|
488
513
|
}
|
|
@@ -493,22 +518,24 @@ export class CDCStream {
|
|
|
493
518
|
}
|
|
494
519
|
|
|
495
520
|
const count = await this.estimatedCountNumber(table);
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
521
|
+
for (const sourceTable of sourceTablesToSnapshot) {
|
|
522
|
+
const updatedSourceTable = await batch.updateTableProgress(sourceTable, {
|
|
523
|
+
totalEstimatedCount: count
|
|
524
|
+
});
|
|
525
|
+
this.tableCache.updateSourceTable(updatedSourceTable);
|
|
526
|
+
tablesToSnapshot.push({ physicalTable: table, sourceTable: updatedSourceTable });
|
|
527
|
+
}
|
|
501
528
|
}
|
|
502
529
|
|
|
503
|
-
for (const
|
|
504
|
-
await this.snapshotTableInTx(batch,
|
|
530
|
+
for (const { physicalTable, sourceTable } of tablesToSnapshot) {
|
|
531
|
+
await this.snapshotTableInTx(batch, physicalTable, sourceTable);
|
|
505
532
|
this.touch();
|
|
506
533
|
}
|
|
507
534
|
|
|
508
535
|
// This will not create a consistent checkpoint yet, but will persist the op.
|
|
509
536
|
// Actual checkpoint will be created when streaming replication caught up.
|
|
510
537
|
const postSnapshotLSN = await getLatestLSN(this.connections);
|
|
511
|
-
await batch.
|
|
538
|
+
await batch.markSnapshotDone(postSnapshotLSN.toString());
|
|
512
539
|
await batch.commit(snapshotLSN!);
|
|
513
540
|
|
|
514
541
|
if (tablesToSnapshot.length > 0) {
|
|
@@ -544,9 +571,11 @@ export class CDCStream {
|
|
|
544
571
|
private async checkSnapshotStatus(): Promise<SnapshotStatusResult> {
|
|
545
572
|
const status = await this.storage.getStatus();
|
|
546
573
|
|
|
547
|
-
if (status.
|
|
574
|
+
if (status.snapshotDone) {
|
|
548
575
|
const additionalTablesToSnapshot: Set<MSSQLSourceTable> = new Set();
|
|
549
|
-
const newTables = this.tableCache
|
|
576
|
+
const newTables = this.tableCache
|
|
577
|
+
.getAll()
|
|
578
|
+
.filter((table) => table.sourceTables.some((sourceTable) => !sourceTable.snapshotComplete));
|
|
550
579
|
if (newTables.length > 0) {
|
|
551
580
|
this.logger.info(
|
|
552
581
|
`Detected new table(s) [${newTables.map((table) => table.toQualifiedName()).join(', ')}] that have not been snapshotted yet.`
|
|
@@ -554,26 +583,26 @@ export class CDCStream {
|
|
|
554
583
|
newTables.forEach((table) => additionalTablesToSnapshot.add(table));
|
|
555
584
|
}
|
|
556
585
|
|
|
557
|
-
// Snapshot is done, but we still need to check that the last known
|
|
586
|
+
// Snapshot is done, but we still need to check that the last known resume LSN is still
|
|
558
587
|
// within the retention threshold of the CDC tables
|
|
559
588
|
|
|
560
|
-
const
|
|
589
|
+
const lastResumeLSN = LSN.fromString(status.resumeLsn!);
|
|
561
590
|
// Check that the CDC tables still have valid data
|
|
562
591
|
const tablesOutsideRetentionThreshold = await checkRetentionThresholds({
|
|
563
|
-
checkpointLSN:
|
|
592
|
+
checkpointLSN: lastResumeLSN,
|
|
564
593
|
tables: this.tableCache.getAll(),
|
|
565
594
|
connectionManager: this.connections
|
|
566
595
|
});
|
|
567
596
|
if (tablesOutsideRetentionThreshold.length > 0) {
|
|
568
597
|
this.logger.warn(
|
|
569
|
-
`Updates from the last
|
|
598
|
+
`Updates from the last resume LSN are no longer available in the CDC instances of the following table(s): ${tablesOutsideRetentionThreshold.map((table) => table.toQualifiedName()).join(', ')}.`
|
|
570
599
|
);
|
|
571
600
|
tablesOutsideRetentionThreshold.forEach((table) => additionalTablesToSnapshot.add(table));
|
|
572
601
|
}
|
|
573
602
|
if (additionalTablesToSnapshot.size > 0) {
|
|
574
603
|
return {
|
|
575
604
|
status: SnapshotStatus.LIMITED_RESNAPSHOT,
|
|
576
|
-
snapshotLSN: status.
|
|
605
|
+
snapshotLSN: status.resumeLsn,
|
|
577
606
|
specificTablesToResnapshot: Array.from(additionalTablesToSnapshot)
|
|
578
607
|
};
|
|
579
608
|
} else {
|
|
@@ -584,8 +613,8 @@ export class CDCStream {
|
|
|
584
613
|
}
|
|
585
614
|
} else {
|
|
586
615
|
return {
|
|
587
|
-
status: status.
|
|
588
|
-
snapshotLSN: status.
|
|
616
|
+
status: status.resumeLsn != null ? SnapshotStatus.RESUME : SnapshotStatus.INITIAL,
|
|
617
|
+
snapshotLSN: status.resumeLsn
|
|
589
618
|
};
|
|
590
619
|
}
|
|
591
620
|
}
|
|
@@ -637,39 +666,45 @@ export class CDCStream {
|
|
|
637
666
|
return {
|
|
638
667
|
onInsert: async (row: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => {
|
|
639
668
|
const afterRow = this.toSqliteRow(row, columns);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
669
|
+
for (const sourceTable of table.getReplicatedSourceTables()) {
|
|
670
|
+
await batch.save({
|
|
671
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
672
|
+
sourceTable,
|
|
673
|
+
before: undefined,
|
|
674
|
+
beforeReplicaId: undefined,
|
|
675
|
+
after: afterRow,
|
|
676
|
+
afterReplicaId: getUuidReplicaIdentityBson(afterRow, sourceTable.replicaIdColumns)
|
|
677
|
+
});
|
|
678
|
+
}
|
|
648
679
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
649
680
|
},
|
|
650
681
|
onUpdate: async (rowAfter: any, rowBefore: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => {
|
|
651
682
|
const beforeRow = this.toSqliteRow(rowBefore, columns);
|
|
652
683
|
const afterRow = this.toSqliteRow(rowAfter, columns);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
684
|
+
for (const sourceTable of table.getReplicatedSourceTables()) {
|
|
685
|
+
await batch.save({
|
|
686
|
+
tag: storage.SaveOperationTag.UPDATE,
|
|
687
|
+
sourceTable,
|
|
688
|
+
before: beforeRow,
|
|
689
|
+
beforeReplicaId: getUuidReplicaIdentityBson(beforeRow, sourceTable.replicaIdColumns),
|
|
690
|
+
after: afterRow,
|
|
691
|
+
afterReplicaId: getUuidReplicaIdentityBson(afterRow, sourceTable.replicaIdColumns)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
661
694
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
662
695
|
},
|
|
663
696
|
onDelete: async (row: any, table: MSSQLSourceTable, columns: sql.IColumnMetadata) => {
|
|
664
697
|
const beforeRow = this.toSqliteRow(row, columns);
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
698
|
+
for (const sourceTable of table.getReplicatedSourceTables()) {
|
|
699
|
+
await batch.save({
|
|
700
|
+
tag: storage.SaveOperationTag.DELETE,
|
|
701
|
+
sourceTable,
|
|
702
|
+
before: beforeRow,
|
|
703
|
+
beforeReplicaId: getUuidReplicaIdentityBson(beforeRow, sourceTable.replicaIdColumns),
|
|
704
|
+
after: undefined,
|
|
705
|
+
afterReplicaId: undefined
|
|
706
|
+
});
|
|
707
|
+
}
|
|
673
708
|
this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED).add(1);
|
|
674
709
|
},
|
|
675
710
|
onCommit: async (lsn: string, transactionCount: number) => {
|
|
@@ -698,7 +733,8 @@ export class CDCStream {
|
|
|
698
733
|
);
|
|
699
734
|
|
|
700
735
|
// Old table needs to be cleaned up
|
|
701
|
-
|
|
736
|
+
const fromTables = fromTable.getReplicatedSourceTables();
|
|
737
|
+
await batch.drop(fromTables);
|
|
702
738
|
this.tableCache.delete(fromTable.objectId);
|
|
703
739
|
|
|
704
740
|
if (change.newTable) {
|
|
@@ -716,13 +752,13 @@ export class CDCStream {
|
|
|
716
752
|
this.logger.info(
|
|
717
753
|
`New CDC capture instance detected for table ${change.table!.toQualifiedName()}. Re-snapshotting table...`
|
|
718
754
|
);
|
|
719
|
-
await batch.drop(
|
|
755
|
+
await batch.drop(change.table!.getReplicatedSourceTables());
|
|
720
756
|
this.tableCache.delete(change.table!.objectId);
|
|
721
757
|
|
|
722
|
-
await this.handleCreateOrUpdateTable(batch, change.table!.
|
|
758
|
+
await this.handleCreateOrUpdateTable(batch, change.table!.ref, change.newCaptureInstance!);
|
|
723
759
|
break;
|
|
724
760
|
case SchemaChangeType.TABLE_DROP:
|
|
725
|
-
await batch.drop(
|
|
761
|
+
await batch.drop(change.table!.getReplicatedSourceTables());
|
|
726
762
|
this.tableCache.delete(change.table!.objectId);
|
|
727
763
|
break;
|
|
728
764
|
case SchemaChangeType.MISSING_CAPTURE_INSTANCE:
|
|
@@ -745,7 +781,7 @@ export class CDCStream {
|
|
|
745
781
|
|
|
746
782
|
private async handleCreateOrUpdateTable(
|
|
747
783
|
batch: storage.BucketStorageBatch,
|
|
748
|
-
table:
|
|
784
|
+
table: SourceTableChangeRef,
|
|
749
785
|
captureInstance: CaptureInstance
|
|
750
786
|
): Promise<void> {
|
|
751
787
|
const replicaIdColumns = await getReplicationIdentityColumns({
|
|
@@ -757,6 +793,7 @@ export class CDCStream {
|
|
|
757
793
|
await this.processTable(
|
|
758
794
|
batch,
|
|
759
795
|
{
|
|
796
|
+
connectionTag: this.connectionTag,
|
|
760
797
|
name: table.name,
|
|
761
798
|
schema: table.schema,
|
|
762
799
|
objectId: table.objectId,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
2
|
import { bson, ColumnDescriptor, SourceTable } from '@powersync/service-core';
|
|
3
3
|
import sql from 'mssql';
|
|
4
|
-
import { MSSQLSourceTable } from '../common/MSSQLSourceTable.js';
|
|
5
4
|
import { MSSQLBaseType } from '../types/mssql-data-types.js';
|
|
6
5
|
import { escapeIdentifier } from '../utils/mssql.js';
|
|
7
6
|
|
|
@@ -23,7 +22,7 @@ export interface MSSQLSnapshotQuery {
|
|
|
23
22
|
export class SimpleSnapshotQuery implements MSSQLSnapshotQuery {
|
|
24
23
|
public constructor(
|
|
25
24
|
private readonly transaction: sql.Transaction,
|
|
26
|
-
private readonly
|
|
25
|
+
private readonly qualifiedTableName: string
|
|
27
26
|
) {}
|
|
28
27
|
|
|
29
28
|
public async initialize(): Promise<void> {}
|
|
@@ -36,7 +35,7 @@ export class SimpleSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
36
35
|
metadataRequest.on('error', reject);
|
|
37
36
|
});
|
|
38
37
|
|
|
39
|
-
metadataRequest.query(`SELECT TOP(0) * FROM ${this.
|
|
38
|
+
metadataRequest.query(`SELECT TOP(0) * FROM ${this.qualifiedTableName}`);
|
|
40
39
|
|
|
41
40
|
const columnMetadata: sql.IColumnMetadata = await metadataPromise;
|
|
42
41
|
yield columnMetadata;
|
|
@@ -44,7 +43,7 @@ export class SimpleSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
44
43
|
const request = this.transaction.request();
|
|
45
44
|
const stream = request.toReadableStream();
|
|
46
45
|
|
|
47
|
-
request.query(`SELECT * FROM ${this.
|
|
46
|
+
request.query(`SELECT * FROM ${this.qualifiedTableName}`);
|
|
48
47
|
|
|
49
48
|
// MSSQL only streams one row at a time
|
|
50
49
|
for await (const row of stream) {
|
|
@@ -84,8 +83,7 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
84
83
|
MSSQLBaseType.BIGINT
|
|
85
84
|
];
|
|
86
85
|
|
|
87
|
-
static supports(
|
|
88
|
-
const sourceTable = table instanceof MSSQLSourceTable ? table.sourceTable : table;
|
|
86
|
+
static supports(sourceTable: SourceTable): boolean {
|
|
89
87
|
if (sourceTable.replicaIdColumns.length != 1) {
|
|
90
88
|
return false;
|
|
91
89
|
}
|
|
@@ -99,11 +97,12 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
99
97
|
|
|
100
98
|
public constructor(
|
|
101
99
|
private readonly transaction: sql.Transaction,
|
|
102
|
-
private readonly
|
|
100
|
+
private readonly qualifiedTableName: string,
|
|
101
|
+
sourceTable: SourceTable,
|
|
103
102
|
private readonly batchSize: number = 10_000,
|
|
104
103
|
lastKeySerialized: Uint8Array | null
|
|
105
104
|
) {
|
|
106
|
-
this.key =
|
|
105
|
+
this.key = sourceTable.replicaIdColumns[0];
|
|
107
106
|
|
|
108
107
|
if (lastKeySerialized != null) {
|
|
109
108
|
this.lastKey = this.deserializeKey(lastKeySerialized);
|
|
@@ -126,7 +125,7 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
126
125
|
metadataRequest.on('recordset', resolve);
|
|
127
126
|
metadataRequest.on('error', reject);
|
|
128
127
|
});
|
|
129
|
-
metadataRequest.query(`SELECT TOP(0) * FROM ${this.
|
|
128
|
+
metadataRequest.query(`SELECT TOP(0) * FROM ${this.qualifiedTableName}`);
|
|
130
129
|
|
|
131
130
|
const columnMetadata: sql.IColumnMetadata = await metadataPromise;
|
|
132
131
|
|
|
@@ -142,7 +141,7 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
142
141
|
const request = this.transaction.request();
|
|
143
142
|
const stream = request.toReadableStream();
|
|
144
143
|
if (this.lastKey == null) {
|
|
145
|
-
request.query(`SELECT TOP(${this.batchSize}) * FROM ${this.
|
|
144
|
+
request.query(`SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} ORDER BY ${escapedKeyName}`);
|
|
146
145
|
} else {
|
|
147
146
|
if (this.key.typeId == null) {
|
|
148
147
|
throw new Error(`typeId required for primary key ${this.key.name}`);
|
|
@@ -150,7 +149,7 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
150
149
|
request
|
|
151
150
|
.input('lastKey', this.lastKey)
|
|
152
151
|
.query(
|
|
153
|
-
`SELECT TOP(${this.batchSize}) * FROM ${this.
|
|
152
|
+
`SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} WHERE ${escapedKeyName} > @lastKey ORDER BY ${escapedKeyName}`
|
|
154
153
|
);
|
|
155
154
|
}
|
|
156
155
|
|
package/src/utils/mssql.ts
CHANGED
|
@@ -308,7 +308,7 @@ export async function getDebugTableInfo(options: GetDebugTableInfoOptions): Prom
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
const idColumns = idColumnsResult?.columns ?? [];
|
|
311
|
-
const sourceTable: sync_rules.
|
|
311
|
+
const sourceTable: sync_rules.SourceTableRef = {
|
|
312
312
|
connectionTag: connectionManager.connectionTag,
|
|
313
313
|
schema: schema,
|
|
314
314
|
name: table.name
|
package/src/utils/schema.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { logger } from '@powersync/lib-services-framework';
|
|
2
|
-
import { SourceEntityDescriptor } from '@powersync/service-core';
|
|
3
2
|
import { TablePattern } from '@powersync/service-sync-rules';
|
|
4
3
|
import sql from 'mssql';
|
|
5
4
|
import { MSSQLConnectionManager } from '../replication/MSSQLConnectionManager.js';
|
|
@@ -139,7 +138,13 @@ export async function getReplicationIdentityColumns(
|
|
|
139
138
|
};
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
export
|
|
141
|
+
export interface SourceTableChangeRef {
|
|
142
|
+
objectId: number | string | undefined;
|
|
143
|
+
schema: string;
|
|
144
|
+
name: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type ResolvedTable = SourceTableChangeRef;
|
|
143
148
|
|
|
144
149
|
export async function getTablesFromPattern(
|
|
145
150
|
connectionManager: MSSQLConnectionManager,
|