@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/api/MSSQLRouteAPIAdapter.d.ts +3 -4
  3. package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
  4. package/dist/common/MSSQLSourceTable.d.ts +13 -3
  5. package/dist/common/MSSQLSourceTable.js +25 -8
  6. package/dist/common/MSSQLSourceTable.js.map +1 -1
  7. package/dist/common/MSSQLSourceTableCache.js +1 -1
  8. package/dist/common/MSSQLSourceTableCache.js.map +1 -1
  9. package/dist/replication/CDCPoller.d.ts +2 -2
  10. package/dist/replication/CDCPoller.js +1 -1
  11. package/dist/replication/CDCPoller.js.map +1 -1
  12. package/dist/replication/CDCReplicationJob.js +1 -1
  13. package/dist/replication/CDCReplicationJob.js.map +1 -1
  14. package/dist/replication/CDCReplicator.js +1 -1
  15. package/dist/replication/CDCReplicator.js.map +1 -1
  16. package/dist/replication/CDCStream.js +103 -81
  17. package/dist/replication/CDCStream.js.map +1 -1
  18. package/dist/replication/MSSQLSnapshotQuery.d.ts +5 -6
  19. package/dist/replication/MSSQLSnapshotQuery.js +13 -15
  20. package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
  21. package/dist/utils/mssql.js.map +1 -1
  22. package/dist/utils/schema.d.ts +6 -2
  23. package/dist/utils/schema.js.map +1 -1
  24. package/package.json +11 -21
  25. package/src/api/MSSQLRouteAPIAdapter.ts +3 -4
  26. package/src/common/MSSQLSourceTable.ts +30 -7
  27. package/src/common/MSSQLSourceTableCache.ts +1 -1
  28. package/src/replication/CDCPoller.ts +3 -4
  29. package/src/replication/CDCReplicationJob.ts +1 -1
  30. package/src/replication/CDCReplicator.ts +1 -1
  31. package/src/replication/CDCStream.ts +125 -88
  32. package/src/replication/MSSQLSnapshotQuery.ts +10 -11
  33. package/src/utils/mssql.ts +1 -1
  34. package/src/utils/schema.ts +7 -2
  35. package/test/src/CDCStreamTestContext.ts +20 -20
  36. package/test/src/schema-changes.test.ts +0 -2
  37. package/test/src/util.ts +1 -1
  38. package/tsconfig.json +0 -3
  39. package/tsconfig.tsbuildinfo +1 -1
@@ -15,7 +15,7 @@ import {
15
15
  storage
16
16
  } from '@powersync/service-core';
17
17
 
18
- import { HydratedSyncRules, SqliteInputRow, SqliteRow, TablePattern } from '@powersync/service-sync-rules';
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 { getReplicationIdentityColumns, getTablesFromPattern, ResolvedTable } from '../utils/schema.js';
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: HydratedSyncRules;
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.group_id;
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?.instances[0] ?? null,
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 this.storage.resolveTable({
240
- group_id: this.groupId,
255
+ const resolved = await batch.resolveTables({
241
256
  connection_id: this.connectionId,
242
- connection_tag: this.connectionTag,
243
- entity_descriptor: table,
244
- sync_rules: this.syncRules
257
+ source: table
245
258
  });
246
- const resolvedTable = new MSSQLSourceTable(resolved.table);
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 shouldSnapshot =
265
- snapshot && !resolved.table.snapshotComplete && resolved.table.syncAny && resolvedTable.enabledForCDC();
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 this table in case a previous snapshot was interrupted.
269
- await batch.truncate([resolved.table]);
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
- await this.snapshotTableInTx(batch, resolvedTable);
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(batch: storage.BucketStorageBatch, table: MSSQLSourceTable): Promise<void> {
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, table);
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([table.sourceTable], postSnapshotLSN.toString());
323
+ const [updatedSourceTable] = await batch.markTableSnapshotDone([sourceTable], postSnapshotLSN.toString());
303
324
  this.logger.info(
304
- `Snapshot of ${table.toQualifiedName()} completed. Post-snapshot LSN: ${postSnapshotLSN.toString()}`
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
- table: MSSQLSourceTable
337
+ physicalTable: MSSQLSourceTable,
338
+ sourceTable: storage.SourceTable
317
339
  ) {
318
- let totalEstimatedCount = table.sourceTable.snapshotStatus?.totalEstimatedCount;
319
- let replicatedCount = table.sourceTable.snapshotStatus?.replicatedCount ?? 0;
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(table)) {
347
+ if (BatchedSnapshotQuery.supports(sourceTable)) {
326
348
  // Single primary key - we can use the primary key for chunking
327
- const orderByKey = table.sourceTable.replicaIdColumns[0];
349
+ const orderByKey = sourceTable.replicaIdColumns[0];
328
350
  query = new BatchedSnapshotQuery(
329
351
  transaction,
330
- table,
352
+ physicalTable.toQualifiedName(),
353
+ sourceTable,
331
354
  this.snapshotBatchSize,
332
- table.sourceTable.snapshotStatus?.lastKey ?? null
355
+ sourceTable.snapshotStatus?.lastKey ?? null
333
356
  );
334
- if (table.sourceTable.snapshotStatus?.lastKey != null) {
357
+ if (sourceTable.snapshotStatus?.lastKey != null) {
335
358
  this.logger.info(
336
- `Snapshotting ${table.toQualifiedName()} ${table.sourceTable.formatSnapshotProgress()} - resuming from ${orderByKey.name} > ${(query as BatchedSnapshotQuery).lastKey}`
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 ${table.toQualifiedName()} ${table.sourceTable.formatSnapshotProgress()} - resumable`
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 ${table.toQualifiedName()} ${table.sourceTable.formatSnapshotProgress()} - not resumable`
369
+ `Snapshotting ${physicalTable.toQualifiedName()} ${sourceTable.formatSnapshotProgress()} - not resumable`
347
370
  );
348
- query = new SimpleSnapshotQuery(transaction, table);
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: table.sourceTable,
398
+ sourceTable,
376
399
  before: undefined,
377
400
  beforeReplicaId: undefined,
378
401
  after: row,
379
- afterReplicaId: getUuidReplicaIdentityBson(row, table.sourceTable.replicaIdColumns)
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(table, transaction);
425
+ totalEstimatedCount = await this.estimatedCountNumber(physicalTable, transaction);
403
426
  lastCountTime = performance.now();
404
427
  }
405
- const updatedSourceTable = await batch.updateTableProgress(table.sourceTable, {
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 ${table.toQualifiedName()} ${table.sourceTable.formatSnapshotProgress()}`);
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([table.sourceTable]);
498
+ await batch.drop(table.getReplicatedSourceTables());
475
499
  // Update table in the table cache
476
- await this.processTable(batch, table.sourceTable, table.captureInstance, false);
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
- if (table.sourceTable.snapshotComplete) {
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 updatedSourceTable = await batch.updateTableProgress(table.sourceTable, {
497
- totalEstimatedCount: count
498
- });
499
- this.tableCache.updateSourceTable(updatedSourceTable);
500
- tablesToSnapshot.push(table);
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 table of tablesToSnapshot) {
504
- await this.snapshotTableInTx(batch, table);
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.markAllSnapshotDone(postSnapshotLSN.toString());
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.snapshot_done && status.checkpoint_lsn) {
574
+ if (status.snapshotDone) {
548
575
  const additionalTablesToSnapshot: Set<MSSQLSourceTable> = new Set();
549
- const newTables = this.tableCache.getAll().filter((table) => !table.sourceTable.snapshotComplete);
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 checkpoint LSN is still
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 lastCheckpointLSN = LSN.fromString(status.checkpoint_lsn);
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: lastCheckpointLSN,
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 checkpoint are no longer available in the CDC instances of the following table(s): ${tablesOutsideRetentionThreshold.map((table) => table.toQualifiedName()).join(', ')}.`
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.checkpoint_lsn,
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.snapshot_lsn != null ? SnapshotStatus.RESUME : SnapshotStatus.INITIAL,
588
- snapshotLSN: status.snapshot_lsn
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
- await batch.save({
641
- tag: storage.SaveOperationTag.INSERT,
642
- sourceTable: table.sourceTable,
643
- before: undefined,
644
- beforeReplicaId: undefined,
645
- after: afterRow,
646
- afterReplicaId: getUuidReplicaIdentityBson(afterRow, table.sourceTable.replicaIdColumns)
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
- await batch.save({
654
- tag: storage.SaveOperationTag.UPDATE,
655
- sourceTable: table.sourceTable,
656
- before: beforeRow,
657
- beforeReplicaId: getUuidReplicaIdentityBson(beforeRow, table.sourceTable.replicaIdColumns),
658
- after: afterRow,
659
- afterReplicaId: getUuidReplicaIdentityBson(afterRow, table.sourceTable.replicaIdColumns)
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
- await batch.save({
666
- tag: storage.SaveOperationTag.DELETE,
667
- sourceTable: table.sourceTable,
668
- before: beforeRow,
669
- beforeReplicaId: getUuidReplicaIdentityBson(beforeRow, table.sourceTable.replicaIdColumns),
670
- after: undefined,
671
- afterReplicaId: undefined
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
- await batch.drop([fromTable.sourceTable]);
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([change.table!.sourceTable]);
755
+ await batch.drop(change.table!.getReplicatedSourceTables());
720
756
  this.tableCache.delete(change.table!.objectId);
721
757
 
722
- await this.handleCreateOrUpdateTable(batch, change.table!.sourceTable, change.newCaptureInstance!);
758
+ await this.handleCreateOrUpdateTable(batch, change.table!.ref, change.newCaptureInstance!);
723
759
  break;
724
760
  case SchemaChangeType.TABLE_DROP:
725
- await batch.drop([change.table!.sourceTable]);
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: Omit<SourceEntityDescriptor, 'replicaIdColumns'>,
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 table: MSSQLSourceTable
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.table.toQualifiedName()}`);
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.table.toQualifiedName()}`);
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(table: SourceTable | MSSQLSourceTable): boolean {
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 table: MSSQLSourceTable,
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 = table.sourceTable.replicaIdColumns[0];
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.table.toQualifiedName()}`);
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.table.toQualifiedName()} ORDER BY ${escapedKeyName}`);
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.table.toQualifiedName()} WHERE ${escapedKeyName} > @lastKey ORDER BY ${escapedKeyName}`
152
+ `SELECT TOP(${this.batchSize}) * FROM ${this.qualifiedTableName} WHERE ${escapedKeyName} > @lastKey ORDER BY ${escapedKeyName}`
154
153
  );
155
154
  }
156
155
 
@@ -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.SourceTableInterface = {
311
+ const sourceTable: sync_rules.SourceTableRef = {
312
312
  connectionTag: connectionManager.connectionTag,
313
313
  schema: schema,
314
314
  name: table.name
@@ -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 type ResolvedTable = Omit<SourceEntityDescriptor, 'replicaIdColumns'>;
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,