@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.
- package/CHANGELOG.md +113 -6
- package/dist/api/MSSQLRouteAPIAdapter.d.ts +2 -2
- package/dist/api/MSSQLRouteAPIAdapter.js +3 -3
- package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/CaptureInstance.d.ts +14 -0
- package/dist/common/CaptureInstance.js +2 -0
- package/dist/common/CaptureInstance.js.map +1 -0
- package/dist/common/MSSQLSourceTable.d.ts +16 -14
- package/dist/common/MSSQLSourceTable.js +35 -16
- package/dist/common/MSSQLSourceTable.js.map +1 -1
- package/dist/common/MSSQLSourceTableCache.js.map +1 -1
- package/dist/common/mssqls-to-sqlite.d.ts +1 -1
- package/dist/common/mssqls-to-sqlite.js +1 -1
- package/dist/common/mssqls-to-sqlite.js.map +1 -1
- package/dist/module/MSSQLModule.js +4 -4
- package/dist/module/MSSQLModule.js.map +1 -1
- package/dist/replication/CDCPoller.d.ts +45 -23
- package/dist/replication/CDCPoller.js +201 -61
- package/dist/replication/CDCPoller.js.map +1 -1
- package/dist/replication/CDCReplicationJob.d.ts +2 -2
- package/dist/replication/CDCReplicationJob.js +12 -4
- package/dist/replication/CDCReplicationJob.js.map +1 -1
- package/dist/replication/CDCReplicator.d.ts +2 -3
- package/dist/replication/CDCReplicator.js +1 -24
- package/dist/replication/CDCReplicator.js.map +1 -1
- package/dist/replication/CDCStream.d.ts +39 -16
- package/dist/replication/CDCStream.js +200 -103
- package/dist/replication/CDCStream.js.map +1 -1
- package/dist/replication/MSSQLConnectionManager.d.ts +1 -1
- package/dist/replication/MSSQLConnectionManager.js +17 -6
- package/dist/replication/MSSQLConnectionManager.js.map +1 -1
- package/dist/replication/MSSQLConnectionManagerFactory.d.ts +1 -1
- package/dist/replication/MSSQLConnectionManagerFactory.js.map +1 -1
- package/dist/replication/MSSQLSnapshotQuery.d.ts +1 -1
- package/dist/replication/MSSQLSnapshotQuery.js +2 -2
- package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
- package/dist/types/types.d.ts +4 -56
- package/dist/types/types.js +5 -24
- package/dist/types/types.js.map +1 -1
- package/dist/utils/deadlock.d.ts +9 -0
- package/dist/utils/deadlock.js +40 -0
- package/dist/utils/deadlock.js.map +1 -0
- package/dist/utils/mssql.d.ts +36 -18
- package/dist/utils/mssql.js +102 -100
- package/dist/utils/mssql.js.map +1 -1
- package/dist/utils/schema.d.ts +9 -0
- package/dist/utils/schema.js +34 -0
- package/dist/utils/schema.js.map +1 -1
- package/package.json +14 -14
- package/src/api/MSSQLRouteAPIAdapter.ts +4 -4
- package/src/common/CaptureInstance.ts +15 -0
- package/src/common/MSSQLSourceTable.ts +33 -24
- package/src/common/MSSQLSourceTableCache.ts +1 -1
- package/src/common/mssqls-to-sqlite.ts +1 -1
- package/src/module/MSSQLModule.ts +4 -4
- package/src/replication/CDCPoller.ts +275 -75
- package/src/replication/CDCReplicationJob.ts +13 -6
- package/src/replication/CDCReplicator.ts +2 -28
- package/src/replication/CDCStream.ts +271 -128
- package/src/replication/MSSQLConnectionManager.ts +17 -7
- package/src/replication/MSSQLConnectionManagerFactory.ts +1 -1
- package/src/replication/MSSQLSnapshotQuery.ts +3 -3
- package/src/types/types.ts +5 -28
- package/src/utils/deadlock.ts +44 -0
- package/src/utils/mssql.ts +163 -128
- package/src/utils/schema.ts +44 -1
- package/test/src/CDCStream.test.ts +10 -8
- package/test/src/CDCStreamTestContext.ts +32 -12
- package/test/src/CDCStream_resumable_snapshot.test.ts +14 -12
- package/test/src/LSN.test.ts +1 -1
- package/test/src/env.ts +1 -1
- package/test/src/mssql-to-sqlite.test.ts +25 -17
- package/test/src/schema-changes.test.ts +474 -0
- package/test/src/util.ts +81 -18
- package/test/tsconfig.json +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 {
|
|
7
|
+
import { describe, expect, test } from 'vitest';
|
|
9
8
|
import { CDCStreamTestContext } from './CDCStreamTestContext.js';
|
|
10
|
-
import {
|
|
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 (
|
|
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(
|
|
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(
|
|
20
|
+
await testResumingReplication(config, 8000);
|
|
21
21
|
});
|
|
22
22
|
});
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
async function testResumingReplication(
|
|
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, {
|
|
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
|
});
|
package/test/src/LSN.test.ts
CHANGED
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:
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
490
|
+
const captureInstances = await getCaptureInstances({
|
|
486
491
|
connectionManager,
|
|
487
|
-
|
|
488
|
-
|
|
492
|
+
table: {
|
|
493
|
+
schema: connectionManager.schema,
|
|
494
|
+
name: tableName
|
|
495
|
+
}
|
|
489
496
|
});
|
|
490
|
-
if (
|
|
497
|
+
if (captureInstances.size === 0) {
|
|
491
498
|
throw new Error(`No CDC capture instance found for table ${tableName}`);
|
|
492
499
|
}
|
|
493
500
|
|
|
494
|
-
const
|
|
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 ${
|
|
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() },
|