@powersync/service-module-mssql 0.0.0-dev-20251202092152 → 0.0.0-dev-20251208113735
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 +8 -12
- package/dist/api/MSSQLRouteAPIAdapter.js +15 -50
- package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
- package/dist/common/LSN.js +2 -2
- package/dist/common/LSN.js.map +1 -1
- package/dist/common/MSSQLSourceTable.js +4 -4
- package/dist/common/MSSQLSourceTable.js.map +1 -1
- package/dist/common/mssqls-to-sqlite.js +4 -4
- package/dist/common/mssqls-to-sqlite.js.map +1 -1
- package/dist/replication/CDCPoller.js +1 -2
- package/dist/replication/CDCPoller.js.map +1 -1
- package/dist/replication/CDCStream.js +24 -32
- package/dist/replication/CDCStream.js.map +1 -1
- package/dist/replication/MSSQLSnapshotQuery.d.ts +0 -17
- package/dist/replication/MSSQLSnapshotQuery.js +0 -47
- package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
- package/dist/utils/mssql.js +27 -13
- package/dist/utils/mssql.js.map +1 -1
- package/dist/utils/schema.js +31 -15
- package/dist/utils/schema.js.map +1 -1
- package/package.json +9 -9
- package/src/api/MSSQLRouteAPIAdapter.ts +15 -50
- package/src/common/LSN.ts +2 -2
- package/src/common/MSSQLSourceTable.ts +4 -4
- package/src/common/mssqls-to-sqlite.ts +11 -4
- package/src/replication/CDCPoller.ts +1 -2
- package/src/replication/CDCStream.ts +26 -42
- package/src/replication/MSSQLSnapshotQuery.ts +0 -54
- package/src/utils/mssql.ts +28 -15
- package/src/utils/schema.ts +31 -17
- package/test/src/CDCStream.test.ts +5 -5
- package/test/src/CDCStream_resumable_snapshot.test.ts +3 -3
- package/test/src/mssql-to-sqlite.test.ts +28 -27
- package/test/src/util.ts +16 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/ci/init-mssql.sql +0 -50
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { bson, ColumnDescriptor, SourceTable } from '@powersync/service-core';
|
|
2
|
-
import { SqliteValue } from '@powersync/service-sync-rules';
|
|
3
2
|
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
4
3
|
import { MSSQLBaseType } from '../types/mssql-data-types.js';
|
|
5
4
|
import sql from 'mssql';
|
|
@@ -15,8 +14,6 @@ export interface MSSQLSnapshotQuery {
|
|
|
15
14
|
next(): AsyncIterableIterator<sql.IColumnMetadata | sql.IRecordSet<any>>;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
export type PrimaryKeyValue = Record<string, SqliteValue>;
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Snapshot query using a plain SELECT * FROM table
|
|
22
19
|
*
|
|
@@ -177,54 +174,3 @@ export class BatchedSnapshotQuery implements MSSQLSnapshotQuery {
|
|
|
177
174
|
return decoded[this.key.name];
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* This performs a snapshot query using a list of primary keys.
|
|
183
|
-
*
|
|
184
|
-
* This is not used for general snapshots, but is used when we need to re-fetch specific rows
|
|
185
|
-
* during streaming replication.
|
|
186
|
-
*/
|
|
187
|
-
export class IdSnapshotQuery implements MSSQLSnapshotQuery {
|
|
188
|
-
static supports(table: SourceTable | MSSQLSourceTable) {
|
|
189
|
-
// We have the same requirements as BatchedSnapshotQuery.
|
|
190
|
-
// This is typically only used as a fallback when ChunkedSnapshotQuery
|
|
191
|
-
// skipped some rows.
|
|
192
|
-
return BatchedSnapshotQuery.supports(table);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
public constructor(
|
|
196
|
-
private readonly transaction: sql.Transaction,
|
|
197
|
-
private readonly table: MSSQLSourceTable,
|
|
198
|
-
private readonly keys: PrimaryKeyValue[]
|
|
199
|
-
) {}
|
|
200
|
-
|
|
201
|
-
public async initialize(): Promise<void> {
|
|
202
|
-
// No-op
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
public async *next(): AsyncIterableIterator<sql.IColumnMetadata | sql.IRecordSet<any>> {
|
|
206
|
-
const metadataRequest = this.transaction.request();
|
|
207
|
-
metadataRequest.stream = true;
|
|
208
|
-
const metadataPromise = new Promise<sql.IColumnMetadata>((resolve, reject) => {
|
|
209
|
-
metadataRequest.on('recordset', resolve);
|
|
210
|
-
metadataRequest.on('error', reject);
|
|
211
|
-
});
|
|
212
|
-
metadataRequest.query(`SELECT TOP(0) * FROM ${this.table.toQualifiedName()}`);
|
|
213
|
-
const columnMetadata: sql.IColumnMetadata = await metadataPromise;
|
|
214
|
-
yield columnMetadata;
|
|
215
|
-
|
|
216
|
-
const keyDefinition = this.table.sourceTable.replicaIdColumns[0];
|
|
217
|
-
const ids = this.keys.map((record) => record[keyDefinition.name]);
|
|
218
|
-
|
|
219
|
-
const request = this.transaction.request();
|
|
220
|
-
const stream = request.toReadableStream();
|
|
221
|
-
request
|
|
222
|
-
.input('ids', ids)
|
|
223
|
-
.query(`SELECT * FROM ${this.table.toQualifiedName()} WHERE ${escapeIdentifier(keyDefinition.name)} = @ids`);
|
|
224
|
-
|
|
225
|
-
// MSSQL only streams one row at a time
|
|
226
|
-
for await (const row of stream) {
|
|
227
|
-
yield row;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
package/src/utils/mssql.ts
CHANGED
|
@@ -89,8 +89,11 @@ export async function ensurePowerSyncCheckpointsTable(connectionManager: MSSQLCo
|
|
|
89
89
|
try {
|
|
90
90
|
// check if the dbo_powersync_checkpoints table exists
|
|
91
91
|
const { recordset: checkpointsResult } = await connectionManager.query(`
|
|
92
|
-
SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA =
|
|
93
|
-
|
|
92
|
+
SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @tableName;
|
|
93
|
+
`, [
|
|
94
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: connectionManager.schema },
|
|
95
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: POWERSYNC_CHECKPOINTS_TABLE },
|
|
96
|
+
]);
|
|
94
97
|
if (checkpointsResult.length > 0) {
|
|
95
98
|
// Table already exists, check if CDC is enabled
|
|
96
99
|
const isEnabled = await isTableEnabledForCDC({
|
|
@@ -114,7 +117,7 @@ export async function ensurePowerSyncCheckpointsTable(connectionManager: MSSQLCo
|
|
|
114
117
|
// Try to create the table
|
|
115
118
|
try {
|
|
116
119
|
await connectionManager.query(`
|
|
117
|
-
CREATE TABLE ${connectionManager.schema}.${POWERSYNC_CHECKPOINTS_TABLE} (
|
|
120
|
+
CREATE TABLE ${escapeIdentifier(connectionManager.schema)}.${escapeIdentifier(POWERSYNC_CHECKPOINTS_TABLE)} (
|
|
118
121
|
id INT IDENTITY PRIMARY KEY,
|
|
119
122
|
last_updated DATETIME NOT NULL DEFAULT (GETDATE())
|
|
120
123
|
)`);
|
|
@@ -137,7 +140,7 @@ export async function ensurePowerSyncCheckpointsTable(connectionManager: MSSQLCo
|
|
|
137
140
|
|
|
138
141
|
export async function createCheckpoint(connectionManager: MSSQLConnectionManager): Promise<void> {
|
|
139
142
|
await connectionManager.query(`
|
|
140
|
-
MERGE ${connectionManager.schema}.${POWERSYNC_CHECKPOINTS_TABLE} AS target
|
|
143
|
+
MERGE ${escapeIdentifier(connectionManager.schema)}.${escapeIdentifier(POWERSYNC_CHECKPOINTS_TABLE)} AS target
|
|
141
144
|
USING (SELECT 1 AS id) AS source
|
|
142
145
|
ON target.id = source.id
|
|
143
146
|
WHEN MATCHED THEN
|
|
@@ -164,10 +167,12 @@ export async function isTableEnabledForCDC(options: IsTableEnabledForCDCOptions)
|
|
|
164
167
|
SELECT 1 FROM cdc.change_tables ct
|
|
165
168
|
JOIN sys.tables AS tbl ON tbl.object_id = ct.source_object_id
|
|
166
169
|
JOIN sys.schemas AS sch ON sch.schema_id = tbl.schema_id
|
|
167
|
-
WHERE sch.name =
|
|
168
|
-
AND tbl.name =
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
WHERE sch.name = @schema
|
|
171
|
+
AND tbl.name = @tableName
|
|
172
|
+
`, [
|
|
173
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: schema },
|
|
174
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: table },
|
|
175
|
+
]);
|
|
171
176
|
return checkResult.length > 0;
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -227,7 +232,8 @@ export async function isWithinRetentionThreshold(options: IsWithinRetentionThres
|
|
|
227
232
|
|
|
228
233
|
export async function getMinLSN(connectionManager: MSSQLConnectionManager, captureInstance: string): Promise<LSN> {
|
|
229
234
|
const { recordset: result } = await connectionManager.query(
|
|
230
|
-
`SELECT sys.fn_cdc_get_min_lsn(
|
|
235
|
+
`SELECT sys.fn_cdc_get_min_lsn(@captureInstance) AS min_lsn`,
|
|
236
|
+
[{ name: 'captureInstance', type: sql.VarChar(sql.MAX), value: captureInstance }]
|
|
231
237
|
);
|
|
232
238
|
const rawMinLSN: Buffer = result[0].min_lsn;
|
|
233
239
|
return LSN.fromBinary(rawMinLSN);
|
|
@@ -258,11 +264,13 @@ export async function getCaptureInstance(options: GetCaptureInstanceOptions): Pr
|
|
|
258
264
|
sys.tables tbl
|
|
259
265
|
INNER JOIN sys.schemas sch ON tbl.schema_id = sch.schema_id
|
|
260
266
|
INNER JOIN cdc.change_tables ct ON ct.source_object_id = tbl.object_id
|
|
261
|
-
WHERE sch.name =
|
|
262
|
-
AND tbl.name =
|
|
267
|
+
WHERE sch.name = @schema
|
|
268
|
+
AND tbl.name = @tableName
|
|
263
269
|
AND ct.end_lsn IS NULL;
|
|
264
|
-
|
|
265
|
-
|
|
270
|
+
`, [
|
|
271
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: schema },
|
|
272
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: tableName },
|
|
273
|
+
]);
|
|
266
274
|
|
|
267
275
|
if (result.length === 0) {
|
|
268
276
|
return null;
|
|
@@ -301,7 +309,10 @@ export async function getLatestReplicatedLSN(connectionManager: MSSQLConnectionM
|
|
|
301
309
|
* @param identifier
|
|
302
310
|
*/
|
|
303
311
|
export function escapeIdentifier(identifier: string): string {
|
|
304
|
-
|
|
312
|
+
// 1. Replace existing closing brackets ] with ]] to escape them
|
|
313
|
+
// 2. Replace dots . with ].[ to handle qualified names
|
|
314
|
+
// 3. Wrap the whole result in [ ]
|
|
315
|
+
return `[${identifier.replace(/]/g, ']]').replace(/\./g, '].[')}]`;
|
|
305
316
|
}
|
|
306
317
|
|
|
307
318
|
export function toQualifiedTableName(schema: string, tableName: string): string {
|
|
@@ -383,7 +394,9 @@ export async function getDebugTableInfo(options: GetDebugTableInfoOptions): Prom
|
|
|
383
394
|
|
|
384
395
|
let selectError: service_types.ReplicationError | null = null;
|
|
385
396
|
try {
|
|
386
|
-
await connectionManager.query(`SELECT TOP 1 * FROM
|
|
397
|
+
await connectionManager.query(`SELECT TOP 1 * FROM @tableName`, [
|
|
398
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: toQualifiedTableName(schema, table.name) },
|
|
399
|
+
]);
|
|
387
400
|
} catch (e) {
|
|
388
401
|
selectError = { level: 'fatal', message: e.message };
|
|
389
402
|
}
|
package/src/utils/schema.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { SourceEntityDescriptor } from '@powersync/service-core';
|
|
|
2
2
|
import { TablePattern } from '@powersync/service-sync-rules';
|
|
3
3
|
import { MSSQLConnectionManager } from '../replication/MSSQLConnectionManager.js';
|
|
4
4
|
import { MSSQLColumnDescriptor } from '../types/mssql-data-types.js';
|
|
5
|
-
import
|
|
5
|
+
import sql from 'mssql';
|
|
6
6
|
|
|
7
7
|
export interface GetColumnsOptions {
|
|
8
8
|
connectionManager: MSSQLConnectionManager;
|
|
@@ -23,10 +23,13 @@ async function getColumns(options: GetColumnsOptions): Promise<MSSQLColumnDescri
|
|
|
23
23
|
JOIN sys.tables AS tbl ON tbl.object_id = col.object_id
|
|
24
24
|
JOIN sys.schemas AS sch ON sch.schema_id = tbl.schema_id
|
|
25
25
|
JOIN sys.types AS typ ON typ.user_type_id = col.user_type_id
|
|
26
|
-
WHERE sch.name =
|
|
27
|
-
AND tbl.name =
|
|
26
|
+
WHERE sch.name = @schema
|
|
27
|
+
AND tbl.name = @tableName
|
|
28
28
|
ORDER BY col.column_id;
|
|
29
|
-
|
|
29
|
+
`, [
|
|
30
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: schema },
|
|
31
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: tableName },
|
|
32
|
+
]);
|
|
30
33
|
|
|
31
34
|
return columnResults.map((row) => {
|
|
32
35
|
return {
|
|
@@ -65,10 +68,13 @@ export async function getReplicationIdentityColumns(
|
|
|
65
68
|
JOIN sys.index_columns AS idx_col ON idx_col.object_id = idx.object_id AND idx_col.index_id = idx.index_id
|
|
66
69
|
JOIN sys.columns AS col ON col.object_id = idx_col.object_id AND col.column_id = idx_col.column_id
|
|
67
70
|
JOIN sys.types AS typ ON typ.user_type_id = col.user_type_id
|
|
68
|
-
WHERE sch.name =
|
|
69
|
-
AND tbl.name =
|
|
71
|
+
WHERE sch.name = @schema
|
|
72
|
+
AND tbl.name = @tableName
|
|
70
73
|
ORDER BY idx_col.key_ordinal;
|
|
71
|
-
|
|
74
|
+
`, [
|
|
75
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: schema },
|
|
76
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: tableName },
|
|
77
|
+
]);
|
|
72
78
|
|
|
73
79
|
if (primaryKeyColumns.length > 0) {
|
|
74
80
|
return {
|
|
@@ -95,10 +101,13 @@ export async function getReplicationIdentityColumns(
|
|
|
95
101
|
JOIN sys.index_columns AS idx_col ON idx_col.object_id = idx.object_id AND idx_col.index_id = idx.index_id
|
|
96
102
|
JOIN sys.columns AS col ON col.object_id = idx_col.object_id AND col.column_id = idx_col.column_id
|
|
97
103
|
JOIN sys.types AS typ ON typ.user_type_id = col.user_type_id
|
|
98
|
-
WHERE sch.name =
|
|
99
|
-
AND tbl.name =
|
|
104
|
+
WHERE sch.name = @schema
|
|
105
|
+
AND tbl.name = @tableName
|
|
100
106
|
ORDER BY idx_col.key_ordinal;
|
|
101
|
-
|
|
107
|
+
`, [
|
|
108
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: schema },
|
|
109
|
+
{ name: 'tableName', type: sql.VarChar(sql.MAX), value: tableName },
|
|
110
|
+
]);
|
|
102
111
|
|
|
103
112
|
if (uniqueKeyColumns.length > 0) {
|
|
104
113
|
return {
|
|
@@ -134,9 +143,12 @@ export async function getTablesFromPattern(
|
|
|
134
143
|
tbl.object_id AS object_id
|
|
135
144
|
FROM sys.tables tbl
|
|
136
145
|
JOIN sys.schemas sch ON tbl.schema_id = sch.schema_id
|
|
137
|
-
WHERE sch.name =
|
|
138
|
-
AND tbl.name LIKE
|
|
139
|
-
|
|
146
|
+
WHERE sch.name = @schema
|
|
147
|
+
AND tbl.name LIKE @tablePattern
|
|
148
|
+
`, [
|
|
149
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: tablePattern.schema },
|
|
150
|
+
{ name: 'tablePattern', type: sql.VarChar(sql.MAX), value: tablePattern.tablePattern },
|
|
151
|
+
]);
|
|
140
152
|
|
|
141
153
|
return tableResults
|
|
142
154
|
.map((row) => {
|
|
@@ -156,10 +168,12 @@ export async function getTablesFromPattern(
|
|
|
156
168
|
tbl.object_id AS object_id
|
|
157
169
|
FROM sys.tables tbl
|
|
158
170
|
JOIN sys.schemas sch ON tbl.schema_id = sch.schema_id
|
|
159
|
-
|
|
160
|
-
AND tbl.name =
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
WHERE sch.name = @schema
|
|
172
|
+
AND tbl.name = @tablePattern
|
|
173
|
+
`, [
|
|
174
|
+
{ name: 'schema', type: sql.VarChar(sql.MAX), value: tablePattern.schema },
|
|
175
|
+
{ name: 'tablePattern', type: sql.VarChar(sql.MAX), value: tablePattern.tablePattern },
|
|
176
|
+
]);
|
|
163
177
|
|
|
164
178
|
return tableResults.map((row) => {
|
|
165
179
|
return {
|
|
@@ -4,7 +4,7 @@ import { ReplicationMetric } from '@powersync/service-types';
|
|
|
4
4
|
import { createTestTable, describeWithStorage, insertTestData, waitForPendingCDCChanges } from './util.js';
|
|
5
5
|
import { storage } from '@powersync/service-core';
|
|
6
6
|
import { CDCStreamTestContext } from './CDCStreamTestContext.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getLatestLSN } from '@module/utils/mssql.js';
|
|
8
8
|
import sql from 'mssql';
|
|
9
9
|
|
|
10
10
|
const BASIC_SYNC_RULES = `
|
|
@@ -25,7 +25,7 @@ function defineCDCStreamTests(factory: storage.TestStorageFactory) {
|
|
|
25
25
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
26
26
|
|
|
27
27
|
await createTestTable(connectionManager, 'test_data');
|
|
28
|
-
const beforeLSN = await
|
|
28
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
29
29
|
const testData = await insertTestData(connectionManager, 'test_data');
|
|
30
30
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
31
31
|
const startRowCount = (await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0;
|
|
@@ -69,7 +69,7 @@ function defineCDCStreamTests(factory: storage.TestStorageFactory) {
|
|
|
69
69
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
70
70
|
|
|
71
71
|
await createTestTable(connectionManager, 'test_data');
|
|
72
|
-
const beforeLSN = await
|
|
72
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
73
73
|
const testData = await insertTestData(connectionManager, 'test_data');
|
|
74
74
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
75
75
|
await context.replicateSnapshot();
|
|
@@ -93,7 +93,7 @@ function defineCDCStreamTests(factory: storage.TestStorageFactory) {
|
|
|
93
93
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
94
94
|
|
|
95
95
|
await createTestTable(connectionManager, 'test_data');
|
|
96
|
-
const beforeLSN = await
|
|
96
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
97
97
|
const testData = await insertTestData(connectionManager, 'test_data');
|
|
98
98
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
99
99
|
await context.replicateSnapshot();
|
|
@@ -120,8 +120,8 @@ function defineCDCStreamTests(factory: storage.TestStorageFactory) {
|
|
|
120
120
|
await createTestTable(connectionManager, 'test_data_1');
|
|
121
121
|
await createTestTable(connectionManager, 'test_data_2');
|
|
122
122
|
|
|
123
|
-
const beforeLSN = await getLatestReplicatedLSN(connectionManager);
|
|
124
123
|
const testData11 = await insertTestData(connectionManager, 'test_data_1');
|
|
124
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
125
125
|
const testData21 = await insertTestData(connectionManager, 'test_data_2');
|
|
126
126
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
127
127
|
|
|
@@ -7,7 +7,7 @@ import { ReplicationMetric } from '@powersync/service-types';
|
|
|
7
7
|
import * as timers from 'node:timers/promises';
|
|
8
8
|
import { logger, ReplicationAbortedError } from '@powersync/lib-services-framework';
|
|
9
9
|
import { CDCStreamTestContext } from './CDCStreamTestContext.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getLatestLSN } from '@module/utils/mssql.js';
|
|
11
11
|
|
|
12
12
|
describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
|
|
13
13
|
describeWithStorage({ timeout: 240_000 }, function (factory) {
|
|
@@ -47,7 +47,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
47
47
|
await createTestTableWithBasicId(connectionManager, 'test_data2');
|
|
48
48
|
|
|
49
49
|
await connectionManager.query(`INSERT INTO test_data1(description) SELECT 'value' FROM GENERATE_SERIES(1, 1000, 1)`);
|
|
50
|
-
let beforeLSN = await
|
|
50
|
+
let beforeLSN = await getLatestLSN(connectionManager);
|
|
51
51
|
await connectionManager.query(`INSERT INTO test_data2(description) SELECT 'value' FROM GENERATE_SERIES(1, 10000, 1)`);
|
|
52
52
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
53
53
|
|
|
@@ -99,7 +99,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
99
99
|
);
|
|
100
100
|
const id2 = updateResult.id;
|
|
101
101
|
logger.info(`Updated row with id: ${id2}`);
|
|
102
|
-
beforeLSN = await
|
|
102
|
+
beforeLSN = await getLatestLSN(context2.connectionManager);
|
|
103
103
|
const {
|
|
104
104
|
recordset: [insertResult]
|
|
105
105
|
} = await context2.connectionManager.query(
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { SqliteInputRow } from '@powersync/service-sync-rules';
|
|
1
|
+
import { SQLITE_TRUE, SqliteInputRow } from '@powersync/service-sync-rules';
|
|
2
2
|
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
|
|
3
3
|
import { clearTestDb, createUpperCaseUUID, TEST_CONNECTION_OPTIONS, waitForPendingCDCChanges } from './util.js';
|
|
4
4
|
import { CDCToSqliteRow, toSqliteInputRow } from '@module/common/mssqls-to-sqlite.js';
|
|
5
5
|
import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
|
|
6
6
|
import {
|
|
7
7
|
enableCDCForTable,
|
|
8
|
+
escapeIdentifier,
|
|
8
9
|
getCaptureInstance,
|
|
10
|
+
getLatestLSN,
|
|
9
11
|
getLatestReplicatedLSN,
|
|
10
12
|
getMinLSN,
|
|
11
13
|
toQualifiedTableName
|
|
@@ -25,7 +27,7 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
25
27
|
|
|
26
28
|
async function setupTestTable() {
|
|
27
29
|
await connectionManager.query(`
|
|
28
|
-
CREATE TABLE ${connectionManager.schema}.test_data (
|
|
30
|
+
CREATE TABLE ${escapeIdentifier(connectionManager.schema)}.test_data (
|
|
29
31
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
30
32
|
tinyint_col TINYINT,
|
|
31
33
|
smallint_col SMALLINT,
|
|
@@ -74,9 +76,9 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
test('Number types mappings', async () => {
|
|
77
|
-
const beforeLSN = await
|
|
79
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
78
80
|
await connectionManager.query(`
|
|
79
|
-
INSERT INTO ${connectionManager.schema}.test_data(
|
|
81
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(
|
|
80
82
|
tinyint_col,
|
|
81
83
|
smallint_col,
|
|
82
84
|
int_col,
|
|
@@ -118,16 +120,16 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
118
120
|
numeric_col: 12345.67,
|
|
119
121
|
money_col: 12345.67,
|
|
120
122
|
smallmoney_col: 123.45,
|
|
121
|
-
bit_col:
|
|
123
|
+
bit_col: SQLITE_TRUE
|
|
122
124
|
};
|
|
123
125
|
expect(databaseRows[0]).toMatchObject(expectedResult);
|
|
124
126
|
expect(replicatedRows[0]).toMatchObject(expectedResult);
|
|
125
127
|
});
|
|
126
128
|
|
|
127
129
|
test('Character types mappings', async () => {
|
|
128
|
-
const beforeLSN = await
|
|
130
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
129
131
|
await connectionManager.query(`
|
|
130
|
-
INSERT INTO
|
|
132
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data (
|
|
131
133
|
char_col,
|
|
132
134
|
varchar_col,
|
|
133
135
|
varchar_max_col,
|
|
@@ -167,11 +169,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
167
169
|
});
|
|
168
170
|
|
|
169
171
|
test('Binary types mappings', async () => {
|
|
170
|
-
const beforeLSN = await
|
|
172
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
171
173
|
const binaryData = Buffer.from('BinaryData');
|
|
172
174
|
await connectionManager.query(
|
|
173
175
|
`
|
|
174
|
-
INSERT INTO
|
|
176
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data (
|
|
175
177
|
binary_col,
|
|
176
178
|
varbinary_col,
|
|
177
179
|
varbinary_max_col,
|
|
@@ -211,11 +213,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
211
213
|
});
|
|
212
214
|
|
|
213
215
|
test('Date types mappings', async () => {
|
|
214
|
-
const beforeLSN = await
|
|
216
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
215
217
|
const testDate = new Date('2023-03-06T15:47:00.000Z');
|
|
216
218
|
await connectionManager.query(
|
|
217
219
|
`
|
|
218
|
-
INSERT INTO
|
|
220
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(
|
|
219
221
|
date_col,
|
|
220
222
|
datetime_col,
|
|
221
223
|
datetime2_col,
|
|
@@ -255,22 +257,21 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
255
257
|
});
|
|
256
258
|
|
|
257
259
|
test('Date types edge cases mappings', async () => {
|
|
258
|
-
const beforeLSN = await getLatestReplicatedLSN(connectionManager);
|
|
259
|
-
|
|
260
260
|
await connectionManager.query(`
|
|
261
|
-
INSERT INTO
|
|
261
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime2_col)
|
|
262
262
|
VALUES ('0001-01-01 00:00:00.000')
|
|
263
263
|
`);
|
|
264
264
|
await connectionManager.query(`
|
|
265
|
-
INSERT INTO
|
|
265
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime2_col)
|
|
266
266
|
VALUES ('9999-12-31 23:59:59.999')
|
|
267
267
|
`);
|
|
268
268
|
await connectionManager.query(`
|
|
269
|
-
INSERT INTO
|
|
269
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime_col)
|
|
270
270
|
VALUES ('1753-01-01 00:00:00')
|
|
271
271
|
`);
|
|
272
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
272
273
|
await connectionManager.query(`
|
|
273
|
-
INSERT INTO
|
|
274
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime_col)
|
|
274
275
|
VALUES ('9999-12-31 23:59:59.997')
|
|
275
276
|
`);
|
|
276
277
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
@@ -292,10 +293,10 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
292
293
|
});
|
|
293
294
|
|
|
294
295
|
test('DateTimeOffset type mapping', async () => {
|
|
295
|
-
const beforeLSN = await
|
|
296
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
296
297
|
// DateTimeOffset preserves timezone information
|
|
297
298
|
await connectionManager.query(`
|
|
298
|
-
INSERT INTO
|
|
299
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetimeoffset_col)
|
|
299
300
|
VALUES ('2023-03-06 15:47:00.000 +05:00')
|
|
300
301
|
`);
|
|
301
302
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
@@ -313,12 +314,12 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
313
314
|
});
|
|
314
315
|
|
|
315
316
|
test('UniqueIdentifier type mapping', async () => {
|
|
316
|
-
const beforeLSN = await
|
|
317
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
317
318
|
|
|
318
319
|
const testGuid = createUpperCaseUUID();
|
|
319
320
|
await connectionManager.query(
|
|
320
321
|
`
|
|
321
|
-
INSERT INTO
|
|
322
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(uniqueidentifier_col)
|
|
322
323
|
VALUES (@guid)
|
|
323
324
|
`,
|
|
324
325
|
[{ name: 'guid', type: sql.UniqueIdentifier, value: testGuid }]
|
|
@@ -334,11 +335,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
334
335
|
});
|
|
335
336
|
|
|
336
337
|
test('JSON type mapping', async () => {
|
|
337
|
-
const beforeLSN = await
|
|
338
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
338
339
|
const expectedJSON = { name: 'John Doe', age: 30, married: true };
|
|
339
340
|
await connectionManager.query(
|
|
340
341
|
`
|
|
341
|
-
INSERT INTO
|
|
342
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(json_col)
|
|
342
343
|
VALUES (@json)
|
|
343
344
|
`,
|
|
344
345
|
[{ name: 'json', type: sql.NVarChar(sql.MAX), value: JSON.stringify(expectedJSON) }]
|
|
@@ -355,11 +356,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
355
356
|
});
|
|
356
357
|
|
|
357
358
|
test('XML type mapping', async () => {
|
|
358
|
-
const beforeLSN = await
|
|
359
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
359
360
|
const xmlData = '<root><item>value</item></root>';
|
|
360
361
|
await connectionManager.query(
|
|
361
362
|
`
|
|
362
|
-
INSERT INTO
|
|
363
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(xml_col)
|
|
363
364
|
VALUES (@xml)
|
|
364
365
|
`,
|
|
365
366
|
[{ name: 'xml', type: sql.Xml, value: xmlData }]
|
|
@@ -375,7 +376,7 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
375
376
|
|
|
376
377
|
// TODO: Update test when properly converting spatial types
|
|
377
378
|
// test('Spatial types mappings', async () => {
|
|
378
|
-
// const beforeLSN = await
|
|
379
|
+
// const beforeLSN = await getLatestLSN(connectionManager);
|
|
379
380
|
// // Geometry and Geography types are stored as binary/WKT strings
|
|
380
381
|
// await connectionManager.query(`
|
|
381
382
|
// INSERT INTO [${connectionManager.schema}].test_data(geometry_col, geography_col)
|
|
@@ -399,7 +400,7 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
399
400
|
// TODO: Enable when HierarchyID type is properly supported
|
|
400
401
|
// test('HierarchyID type mapping', async () => {
|
|
401
402
|
// const hierarchyid = '/1/';
|
|
402
|
-
// const beforeLSN = await
|
|
403
|
+
// const beforeLSN = await getLatestLSN(connectionManager);
|
|
403
404
|
// await connectionManager.query(`
|
|
404
405
|
// INSERT INTO [${connectionManager.schema}].test_data(hierarchyid_col)
|
|
405
406
|
// VALUES (@hierarchyid)
|
package/test/src/util.ts
CHANGED
|
@@ -8,7 +8,7 @@ import * as postgres_storage from '@powersync/service-module-postgres-storage';
|
|
|
8
8
|
import { describe, TestOptions } from 'vitest';
|
|
9
9
|
import { env } from './env.js';
|
|
10
10
|
import { MSSQLConnectionManager } from '@module/replication/MSSQLConnectionManager.js';
|
|
11
|
-
import { createCheckpoint, enableCDCForTable, getLatestLSN } from '@module/utils/mssql.js';
|
|
11
|
+
import { createCheckpoint, enableCDCForTable, escapeIdentifier, getLatestLSN } from '@module/utils/mssql.js';
|
|
12
12
|
import sql from 'mssql';
|
|
13
13
|
import { v4 as uuid } from 'uuid';
|
|
14
14
|
import { LSN } from '@module/common/LSN.js';
|
|
@@ -66,6 +66,16 @@ export async function clearTestDb(connectionManager: MSSQLConnectionManager) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export async function resetTestTable(connectionManager: MSSQLConnectionManager, tableName: string) {
|
|
70
|
+
await connectionManager.execute('sys.sp_cdc_disable_table', [
|
|
71
|
+
{ name: 'source_schema', value: connectionManager.schema },
|
|
72
|
+
{ name: 'source_name', value: tableName },
|
|
73
|
+
{ name: 'capture_instance', value: 'all' }
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
await connectionManager.query(`DROP TABLE [${tableName}]`);
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
/**
|
|
70
80
|
* Create a new database for testing and enables CDC on it.
|
|
71
81
|
* @param connectionManager
|
|
@@ -84,7 +94,7 @@ export async function createTestDb(connectionManager: MSSQLConnectionManager, db
|
|
|
84
94
|
|
|
85
95
|
export async function createTestTable(connectionManager: MSSQLConnectionManager, tableName: string): Promise<void> {
|
|
86
96
|
await connectionManager.query(`
|
|
87
|
-
CREATE TABLE ${connectionManager.schema}.${tableName} (
|
|
97
|
+
CREATE TABLE ${escapeIdentifier(connectionManager.schema)}.${escapeIdentifier(tableName)} (
|
|
88
98
|
id UNIQUEIDENTIFIER PRIMARY KEY,
|
|
89
99
|
description VARCHAR(MAX)
|
|
90
100
|
)
|
|
@@ -97,7 +107,7 @@ export async function createTestTableWithBasicId(
|
|
|
97
107
|
tableName: string
|
|
98
108
|
): Promise<void> {
|
|
99
109
|
await connectionManager.query(`
|
|
100
|
-
CREATE TABLE ${connectionManager.schema}.${tableName} (
|
|
110
|
+
CREATE TABLE ${escapeIdentifier(connectionManager.schema)}.${escapeIdentifier(tableName)} (
|
|
101
111
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
|
102
112
|
description VARCHAR(MAX)
|
|
103
113
|
)
|
|
@@ -114,7 +124,7 @@ export async function insertTestData(connectionManager: MSSQLConnectionManager,
|
|
|
114
124
|
const description = `description_${id}`;
|
|
115
125
|
await connectionManager.query(
|
|
116
126
|
`
|
|
117
|
-
INSERT INTO ${connectionManager.schema}.${tableName} (id, description) VALUES (@id, @description)
|
|
127
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.${escapeIdentifier(tableName)} (id, description) VALUES (@id, @description)
|
|
118
128
|
`,
|
|
119
129
|
[
|
|
120
130
|
{ name: 'id', type: sql.UniqueIdentifier, value: id },
|
|
@@ -141,8 +151,8 @@ export async function waitForPendingCDCChanges(
|
|
|
141
151
|
);
|
|
142
152
|
|
|
143
153
|
if (result.length === 0) {
|
|
144
|
-
logger.info(`CDC changes pending. Waiting for
|
|
145
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
154
|
+
logger.info(`CDC changes pending. Waiting for 200ms...`);
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
146
156
|
} else {
|
|
147
157
|
logger.info(`Found LSN: ${LSN.fromBinary(result[0].start_lsn).toString()}`);
|
|
148
158
|
return;
|