@powersync/service-module-mssql 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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 (76) hide show
  1. package/CHANGELOG.md +113 -6
  2. package/dist/api/MSSQLRouteAPIAdapter.d.ts +2 -2
  3. package/dist/api/MSSQLRouteAPIAdapter.js +3 -3
  4. package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
  5. package/dist/common/CaptureInstance.d.ts +14 -0
  6. package/dist/common/CaptureInstance.js +2 -0
  7. package/dist/common/CaptureInstance.js.map +1 -0
  8. package/dist/common/MSSQLSourceTable.d.ts +16 -14
  9. package/dist/common/MSSQLSourceTable.js +35 -16
  10. package/dist/common/MSSQLSourceTable.js.map +1 -1
  11. package/dist/common/MSSQLSourceTableCache.js.map +1 -1
  12. package/dist/common/mssqls-to-sqlite.d.ts +1 -1
  13. package/dist/common/mssqls-to-sqlite.js +1 -1
  14. package/dist/common/mssqls-to-sqlite.js.map +1 -1
  15. package/dist/module/MSSQLModule.js +4 -4
  16. package/dist/module/MSSQLModule.js.map +1 -1
  17. package/dist/replication/CDCPoller.d.ts +45 -23
  18. package/dist/replication/CDCPoller.js +201 -61
  19. package/dist/replication/CDCPoller.js.map +1 -1
  20. package/dist/replication/CDCReplicationJob.d.ts +2 -2
  21. package/dist/replication/CDCReplicationJob.js +12 -4
  22. package/dist/replication/CDCReplicationJob.js.map +1 -1
  23. package/dist/replication/CDCReplicator.d.ts +2 -3
  24. package/dist/replication/CDCReplicator.js +1 -24
  25. package/dist/replication/CDCReplicator.js.map +1 -1
  26. package/dist/replication/CDCStream.d.ts +39 -16
  27. package/dist/replication/CDCStream.js +200 -103
  28. package/dist/replication/CDCStream.js.map +1 -1
  29. package/dist/replication/MSSQLConnectionManager.d.ts +1 -1
  30. package/dist/replication/MSSQLConnectionManager.js +17 -6
  31. package/dist/replication/MSSQLConnectionManager.js.map +1 -1
  32. package/dist/replication/MSSQLConnectionManagerFactory.d.ts +1 -1
  33. package/dist/replication/MSSQLConnectionManagerFactory.js.map +1 -1
  34. package/dist/replication/MSSQLSnapshotQuery.d.ts +1 -1
  35. package/dist/replication/MSSQLSnapshotQuery.js +2 -2
  36. package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
  37. package/dist/types/types.d.ts +4 -56
  38. package/dist/types/types.js +5 -24
  39. package/dist/types/types.js.map +1 -1
  40. package/dist/utils/deadlock.d.ts +9 -0
  41. package/dist/utils/deadlock.js +40 -0
  42. package/dist/utils/deadlock.js.map +1 -0
  43. package/dist/utils/mssql.d.ts +36 -18
  44. package/dist/utils/mssql.js +102 -100
  45. package/dist/utils/mssql.js.map +1 -1
  46. package/dist/utils/schema.d.ts +9 -0
  47. package/dist/utils/schema.js +34 -0
  48. package/dist/utils/schema.js.map +1 -1
  49. package/package.json +14 -14
  50. package/src/api/MSSQLRouteAPIAdapter.ts +4 -4
  51. package/src/common/CaptureInstance.ts +15 -0
  52. package/src/common/MSSQLSourceTable.ts +33 -24
  53. package/src/common/MSSQLSourceTableCache.ts +1 -1
  54. package/src/common/mssqls-to-sqlite.ts +1 -1
  55. package/src/module/MSSQLModule.ts +4 -4
  56. package/src/replication/CDCPoller.ts +275 -75
  57. package/src/replication/CDCReplicationJob.ts +13 -6
  58. package/src/replication/CDCReplicator.ts +2 -28
  59. package/src/replication/CDCStream.ts +271 -128
  60. package/src/replication/MSSQLConnectionManager.ts +17 -7
  61. package/src/replication/MSSQLConnectionManagerFactory.ts +1 -1
  62. package/src/replication/MSSQLSnapshotQuery.ts +3 -3
  63. package/src/types/types.ts +5 -28
  64. package/src/utils/deadlock.ts +44 -0
  65. package/src/utils/mssql.ts +163 -128
  66. package/src/utils/schema.ts +44 -1
  67. package/test/src/CDCStream.test.ts +10 -8
  68. package/test/src/CDCStreamTestContext.ts +32 -12
  69. package/test/src/CDCStream_resumable_snapshot.test.ts +14 -12
  70. package/test/src/LSN.test.ts +1 -1
  71. package/test/src/env.ts +1 -1
  72. package/test/src/mssql-to-sqlite.test.ts +25 -17
  73. package/test/src/schema-changes.test.ts +474 -0
  74. package/test/src/util.ts +81 -18
  75. package/test/tsconfig.json +0 -1
  76. package/tsconfig.tsbuildinfo +1 -1
@@ -1,3 +1,5 @@
1
+ import { CDCStream, CDCStreamOptions } from '@module/replication/CDCStream.js';
2
+ import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
1
3
  import {
2
4
  BucketStorageFactory,
3
5
  createCoreReplicationMetrics,
@@ -9,11 +11,9 @@ import {
9
11
  SyncRulesBucketStorage,
10
12
  updateSyncRulesFromYaml
11
13
  } from '@powersync/service-core';
12
- import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
13
- import { clearTestDb, getClientCheckpoint, TEST_CONNECTION_OPTIONS } from './util.js';
14
- import { CDCStream, CDCStreamOptions } from '@module/replication/CDCStream.js';
15
- import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
14
+ import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
16
15
  import timers from 'timers/promises';
16
+ import { clearTestDb, getClientCheckpoint, TEST_CONNECTION_OPTIONS } from './util.js';
17
17
 
18
18
  /**
19
19
  * Tests operating on the change data capture need to configure the stream and manage asynchronous
@@ -26,6 +26,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
26
26
  private _cdcStream?: CDCStream;
27
27
  private abortController = new AbortController();
28
28
  private streamPromise?: Promise<void>;
29
+ private syncRulesContent?: storage.PersistedSyncRulesContent;
29
30
  public storage?: SyncRulesBucketStorage;
30
31
  private snapshotPromise?: Promise<void>;
31
32
  private replicationDone = false;
@@ -77,6 +78,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
77
78
  const syncRules = await this.factory.updateSyncRules(
78
79
  updateSyncRulesFromYaml(content, { validate: true, storageVersion: LEGACY_STORAGE_VERSION })
79
80
  );
81
+ this.syncRulesContent = syncRules;
80
82
  this.storage = this.factory.getInstance(syncRules);
81
83
  return this.storage!;
82
84
  }
@@ -84,9 +86,10 @@ export class CDCStreamTestContext implements AsyncDisposable {
84
86
  async loadNextSyncRules() {
85
87
  const syncRules = await this.factory.getNextSyncRulesContent();
86
88
  if (syncRules == null) {
87
- throw new Error(`Next sync rules not available`);
89
+ throw new Error(`Next replication stream not available`);
88
90
  }
89
91
 
92
+ this.syncRulesContent = syncRules;
90
93
  this.storage = this.factory.getInstance(syncRules);
91
94
  return this.storage!;
92
95
  }
@@ -94,13 +97,21 @@ export class CDCStreamTestContext implements AsyncDisposable {
94
97
  async loadActiveSyncRules() {
95
98
  const syncRules = await this.factory.getActiveSyncRulesContent();
96
99
  if (syncRules == null) {
97
- throw new Error(`Active sync rules not available`);
100
+ throw new Error(`Active replication stream not available`);
98
101
  }
99
102
 
103
+ this.syncRulesContent = syncRules;
100
104
  this.storage = this.factory.getInstance(syncRules);
101
105
  return this.storage!;
102
106
  }
103
107
 
108
+ private getSyncRulesContent(): storage.PersistedSyncRulesContent {
109
+ if (this.syncRulesContent == null) {
110
+ throw new Error('Sync config not configured - call updateSyncRules() first');
111
+ }
112
+ return this.syncRulesContent;
113
+ }
114
+
104
115
  get cdcStream() {
105
116
  if (this.storage == null) {
106
117
  throw new Error('updateSyncRules() first');
@@ -118,6 +129,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
118
129
  pollingIntervalMs: 1000,
119
130
  trustServerCertificate: true
120
131
  },
132
+ schemaCheckIntervalMs: 500,
121
133
  ...this.cdcStreamOptions
122
134
  };
123
135
  this._cdcStream = new CDCStream(options);
@@ -129,7 +141,7 @@ export class CDCStreamTestContext implements AsyncDisposable {
129
141
  */
130
142
  async initializeReplication() {
131
143
  await this.replicateSnapshot();
132
- // TODO: renable this.startStreaming();
144
+ await this.startStreaming();
133
145
  // Make sure we're up to date
134
146
  await this.getCheckpoint();
135
147
  }
@@ -139,11 +151,11 @@ export class CDCStreamTestContext implements AsyncDisposable {
139
151
  this.replicationDone = true;
140
152
  }
141
153
 
142
- // TODO: Enable once streaming is implemented
143
154
  startStreaming() {
144
155
  if (!this.replicationDone) {
145
156
  throw new Error('Call replicateSnapshot() before startStreaming()');
146
157
  }
158
+
147
159
  this.streamPromise = this.cdcStream.streamChanges();
148
160
  // Wait for the replication to start before returning.
149
161
  // This avoids a bunch of unpredictable race conditions that appear in testing
@@ -171,7 +183,8 @@ export class CDCStreamTestContext implements AsyncDisposable {
171
183
 
172
184
  async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
173
185
  let checkpoint = await this.getCheckpoint(options);
174
- const map = new Map<string, InternalOpId>(Object.entries(buckets));
186
+ const syncRules = this.getSyncRulesContent();
187
+ const map = Object.entries(buckets).map(([bucket, start]) => bucketRequest(syncRules, bucket, start));
175
188
  return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
176
189
  }
177
190
 
@@ -183,8 +196,9 @@ export class CDCStreamTestContext implements AsyncDisposable {
183
196
  if (typeof start == 'string') {
184
197
  start = BigInt(start);
185
198
  }
199
+ const syncRules = this.getSyncRulesContent();
186
200
  const checkpoint = await this.getCheckpoint(options);
187
- const map = new Map<string, InternalOpId>([[bucket, start]]);
201
+ let map = [bucketRequest(syncRules, bucket, start)];
188
202
  let data: OplogEntry[] = [];
189
203
  while (true) {
190
204
  const batch = this.storage!.getBucketDataBatch(checkpoint, map);
@@ -194,11 +208,16 @@ export class CDCStreamTestContext implements AsyncDisposable {
194
208
  if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
195
209
  break;
196
210
  }
197
- map.set(bucket, BigInt(batches[0]!.chunkData.next_after));
211
+ map = [bucketRequest(syncRules, bucket, BigInt(batches[0]!.chunkData.next_after))];
198
212
  }
199
213
  return data;
200
214
  }
201
215
 
216
+ async getFinalBucketState(bucket: string) {
217
+ const data = await this.getBucketData(bucket);
218
+ return test_utils.reduceBucket(data).slice(1);
219
+ }
220
+
202
221
  /**
203
222
  * This does not wait for a client checkpoint.
204
223
  */
@@ -208,7 +227,8 @@ export class CDCStreamTestContext implements AsyncDisposable {
208
227
  start = BigInt(start);
209
228
  }
210
229
  const { checkpoint } = await this.storage!.getCheckpoint();
211
- const map = new Map<string, InternalOpId>([[bucket, start]]);
230
+ const syncRules = this.getSyncRulesContent();
231
+ const map = [bucketRequest(syncRules, bucket, start)];
212
232
  const batch = this.storage!.getBucketDataBatch(checkpoint, map);
213
233
  const batches = await test_utils.fromAsync(batch);
214
234
  return batches[0]?.chunkData.data ?? [];
@@ -1,28 +1,28 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { env } from './env.js';
3
- import { createTestTableWithBasicId, describeWithStorage, waitForPendingCDCChanges } from './util.js';
4
- import { TestStorageFactory } from '@powersync/service-core';
1
+ import { getLatestLSN } from '@module/utils/mssql.js';
2
+ import { logger, ReplicationAbortedError } from '@powersync/lib-services-framework';
3
+ import { TestStorageConfig } from '@powersync/service-core';
5
4
  import { METRICS_HELPER } from '@powersync/service-core-tests';
6
5
  import { ReplicationMetric } from '@powersync/service-types';
7
6
  import * as timers from 'node:timers/promises';
8
- import { logger, ReplicationAbortedError } from '@powersync/lib-services-framework';
7
+ import { describe, expect, test } from 'vitest';
9
8
  import { CDCStreamTestContext } from './CDCStreamTestContext.js';
10
- import { getLatestLSN } from '@module/utils/mssql.js';
9
+ import { env } from './env.js';
10
+ import { createTestTableWithBasicId, describeWithStorage, waitForPendingCDCChanges } from './util.js';
11
11
 
12
12
  describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
13
- describeWithStorage({ timeout: 240_000 }, function (factory) {
13
+ describeWithStorage({ timeout: 240_000 }, function (config) {
14
14
  test('resuming initial replication (1)', async () => {
15
15
  // Stop early - likely to not include deleted row in first replication attempt.
16
- await testResumingReplication(factory, 2000);
16
+ await testResumingReplication(config, 2000);
17
17
  });
18
18
  test('resuming initial replication (2)', async () => {
19
19
  // Stop late - likely to include deleted row in first replication attempt.
20
- await testResumingReplication(factory, 8000);
20
+ await testResumingReplication(config, 8000);
21
21
  });
22
22
  });
23
23
  });
24
24
 
25
- async function testResumingReplication(factory: TestStorageFactory, stopAfter: number) {
25
+ async function testResumingReplication(config: TestStorageConfig, stopAfter: number) {
26
26
  // This tests interrupting and then resuming initial replication.
27
27
  // We interrupt replication after test_data1 has fully replicated, and
28
28
  // test_data2 has partially replicated.
@@ -34,7 +34,9 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
34
34
  // have been / have not been replicated at that point is not deterministic.
35
35
  // We do allow for some variation in the test results to account for this.
36
36
 
37
- await using context = await CDCStreamTestContext.open(factory, { cdcStreamOptions: { snapshotBatchSize: 1000 } });
37
+ await using context = await CDCStreamTestContext.open(config.factory, {
38
+ cdcStreamOptions: { snapshotBatchSize: 1000 }
39
+ });
38
40
 
39
41
  await context.updateSyncRules(`bucket_definitions:
40
42
  global:
@@ -84,7 +86,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
84
86
  }
85
87
 
86
88
  // Bypass the usual "clear db on factory open" step.
87
- await using context2 = await CDCStreamTestContext.open(factory, {
89
+ await using context2 = await CDCStreamTestContext.open(config.factory, {
88
90
  doNotClear: true,
89
91
  cdcStreamOptions: { snapshotBatchSize: 1000 }
90
92
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, test } from 'vitest';
2
1
  import { LSN } from '@module/common/LSN.js';
2
+ import { describe, expect, test } from 'vitest';
3
3
 
4
4
  describe('LSN', () => {
5
5
  test('normalizes lowercase hex strings to uppercase', () => {
package/test/src/env.ts CHANGED
@@ -5,7 +5,7 @@ export const env = utils.collectEnvironmentVariables({
5
5
  MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'),
6
6
  CI: utils.type.boolean.default('false'),
7
7
  SLOW_TESTS: utils.type.boolean.default('false'),
8
- PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'),
8
+ PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5432/powersync_storage_test'),
9
9
  TEST_MONGO_STORAGE: utils.type.boolean.default('true'),
10
10
  TEST_POSTGRES_STORAGE: utils.type.boolean.default('true')
11
11
  });
@@ -1,3 +1,13 @@
1
+ import { CDCToSqliteRow, toSqliteInputRow } from '@module/common/mssqls-to-sqlite.js';
2
+ import { CDC_SCHEMA } from '@module/common/MSSQLSourceTable.js';
3
+ import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
4
+ import {
5
+ escapeIdentifier,
6
+ getCaptureInstances,
7
+ getLatestLSN,
8
+ getLatestReplicatedLSN,
9
+ toQualifiedTableName
10
+ } from '@module/utils/mssql.js';
1
11
  import {
2
12
  applyRowContext,
3
13
  CompatibilityContext,
@@ -5,20 +15,15 @@ import {
5
15
  SqliteInputRow,
6
16
  TimeValuePrecision
7
17
  } from '@powersync/service-sync-rules';
18
+ import sql from 'mssql';
8
19
  import { afterAll, beforeEach, describe, expect, test } from 'vitest';
9
- import { clearTestDb, createUpperCaseUUID, TEST_CONNECTION_OPTIONS, waitForPendingCDCChanges } from './util.js';
10
- import { CDCToSqliteRow, toSqliteInputRow } from '@module/common/mssqls-to-sqlite.js';
11
- import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
12
20
  import {
21
+ clearTestDb,
22
+ createUpperCaseUUID,
13
23
  enableCDCForTable,
14
- escapeIdentifier,
15
- getCaptureInstance,
16
- getLatestLSN,
17
- getLatestReplicatedLSN,
18
- getMinLSN,
19
- toQualifiedTableName
20
- } from '@module/utils/mssql.js';
21
- import sql from 'mssql';
24
+ TEST_CONNECTION_OPTIONS,
25
+ waitForPendingCDCChanges
26
+ } from './util.js';
22
27
 
23
28
  describe('MSSQL Data Types Tests', () => {
24
29
  const connectionManager = new MSSQLConnectionManager(TEST_CONNECTION_OPTIONS, {});
@@ -482,20 +487,23 @@ async function getReplicatedRows(
482
487
  ): Promise<SqliteInputRow[]> {
483
488
  const endLSN = await getLatestReplicatedLSN(connectionManager);
484
489
 
485
- const captureInstance = await getCaptureInstance({
490
+ const captureInstances = await getCaptureInstances({
486
491
  connectionManager,
487
- schema: connectionManager.schema,
488
- tableName
492
+ table: {
493
+ schema: connectionManager.schema,
494
+ name: tableName
495
+ }
489
496
  });
490
- if (!captureInstance) {
497
+ if (captureInstances.size === 0) {
491
498
  throw new Error(`No CDC capture instance found for table ${tableName}`);
492
499
  }
493
500
 
494
- const startLSN = await getMinLSN(connectionManager, captureInstance.name);
501
+ const captureInstance = Array.from(captureInstances.values())[0].instances[0];
502
+ const startLSN = captureInstance.minLSN;
495
503
  // Query CDC changes
496
504
  const { recordset: results } = await connectionManager.query(
497
505
  `
498
- SELECT * FROM ${captureInstance.schema}.fn_cdc_get_all_changes_${captureInstance.name}(@from_lsn, @to_lsn, 'all update old') ORDER BY __$start_lsn, __$seqval
506
+ SELECT * FROM ${CDC_SCHEMA}.fn_cdc_get_all_changes_${captureInstance.name}(@from_lsn, @to_lsn, 'all update old') ORDER BY __$start_lsn, __$seqval
499
507
  `,
500
508
  [
501
509
  { name: 'from_lsn', type: sql.VarBinary, value: startLSN.toBinary() },