@powersync/service-module-mssql 0.6.3 → 0.7.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 (33) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/common/MSSQLSourceTable.d.ts +13 -3
  3. package/dist/common/MSSQLSourceTable.js +25 -8
  4. package/dist/common/MSSQLSourceTable.js.map +1 -1
  5. package/dist/common/MSSQLSourceTableCache.js +1 -1
  6. package/dist/common/MSSQLSourceTableCache.js.map +1 -1
  7. package/dist/replication/CDCPoller.d.ts +5 -5
  8. package/dist/replication/CDCPoller.js +3 -3
  9. package/dist/replication/CDCPoller.js.map +1 -1
  10. package/dist/replication/CDCReplicationJob.js +2 -2
  11. package/dist/replication/CDCReplicationJob.js.map +1 -1
  12. package/dist/replication/CDCStream.js +95 -73
  13. package/dist/replication/CDCStream.js.map +1 -1
  14. package/dist/replication/MSSQLSnapshotQuery.d.ts +5 -6
  15. package/dist/replication/MSSQLSnapshotQuery.js +13 -15
  16. package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
  17. package/dist/utils/mssql.js.map +1 -1
  18. package/dist/utils/schema.d.ts +6 -2
  19. package/dist/utils/schema.js.map +1 -1
  20. package/package.json +12 -12
  21. package/src/common/MSSQLSourceTable.ts +30 -7
  22. package/src/common/MSSQLSourceTableCache.ts +1 -1
  23. package/src/replication/CDCPoller.ts +8 -9
  24. package/src/replication/CDCReplicationJob.ts +2 -2
  25. package/src/replication/CDCStream.ts +117 -80
  26. package/src/replication/MSSQLSnapshotQuery.ts +10 -11
  27. package/src/utils/mssql.ts +1 -1
  28. package/src/utils/schema.ts +7 -2
  29. package/test/src/CDCStream.test.ts +2 -2
  30. package/test/src/CDCStreamTestContext.ts +3 -3
  31. package/test/src/schema-changes.test.ts +0 -2
  32. package/test/tsconfig.json +0 -1
  33. 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;
@@ -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(
@@ -259,30 +272,38 @@ export class CDCStream {
259
272
  await batch.drop(resolved.dropTables);
260
273
 
261
274
  // Snapshot if:
262
- // 1. The table is in the sync rules and snapshot is requested, or not already done.
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) {
@@ -546,7 +573,9 @@ export class CDCStream {
546
573
 
547
574
  if (status.snapshot_done && status.checkpoint_lsn) {
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.`
@@ -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,
@@ -143,7 +143,7 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
143
143
  ]);
144
144
  });
145
145
 
146
- test('Replication for tables not in the sync rules are ignored', async () => {
146
+ test('Replication for tables not in the sync config are ignored', async () => {
147
147
  await using context = await CDCStreamTestContext.open(factory);
148
148
  const { connectionManager } = context;
149
149
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -164,7 +164,7 @@ function defineCDCStreamTests(config: storage.TestStorageConfig) {
164
164
  const endRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
165
165
  const endTxCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.TRANSACTIONS_REPLICATED)) ?? 0;
166
166
 
167
- // There was a transaction, but it is not counted since it is not for a table in the sync rules
167
+ // There was a transaction, but it is not counted since it is not for a table in the sync config
168
168
  expect(endRowCount - startRowCount).toEqual(0);
169
169
  expect(endTxCount - startTxCount).toEqual(0);
170
170
  });
@@ -86,7 +86,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
86
86
  async loadNextSyncRules() {
87
87
  const syncRules = await this.factory.getNextSyncRulesContent();
88
88
  if (syncRules == null) {
89
- throw new Error(`Next sync rules not available`);
89
+ throw new Error(`Next replication stream not available`);
90
90
  }
91
91
 
92
92
  this.syncRulesContent = syncRules;
@@ -97,7 +97,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
97
97
  async loadActiveSyncRules() {
98
98
  const syncRules = await this.factory.getActiveSyncRulesContent();
99
99
  if (syncRules == null) {
100
- throw new Error(`Active sync rules not available`);
100
+ throw new Error(`Active replication stream not available`);
101
101
  }
102
102
 
103
103
  this.syncRulesContent = syncRules;
@@ -107,7 +107,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
107
107
 
108
108
  private getSyncRulesContent(): storage.PersistedSyncRulesContent {
109
109
  if (this.syncRulesContent == null) {
110
- throw new Error('Sync rules not configured - call updateSyncRules() first');
110
+ throw new Error('Sync config not configured - call updateSyncRules() first');
111
111
  }
112
112
  return this.syncRulesContent;
113
113
  }
@@ -339,9 +339,7 @@ function defineSchemaChangesTests(config: storage.TestStorageConfig) {
339
339
  await context.replicateSnapshot();
340
340
  await context.startStreaming();
341
341
 
342
- const schemaSpy = vi.spyOn(context.cdcStream, 'handleSchemaChange');
343
342
  await enableCDCForTable({ connectionManager, table: 'test_data' });
344
- await expectedSchemaChange(schemaSpy, SchemaChangeType.NEW_CAPTURE_INSTANCE);
345
343
 
346
344
  let data = await context.getBucketData('global[]');
347
345
  expect(data).toMatchObject([putOp('test_data', testData1)]);
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "extends": "../../../tsconfig.tests.json",
3
3
  "compilerOptions": {
4
- "baseUrl": "./",
5
4
  "paths": {
6
5
  "@/*": ["../../../packages/service-core/src/*"],
7
6
  "@module/*": ["../src/*"],