@powersync/service-module-mssql 0.0.0-dev-20251202102946 → 0.0.0-dev-20251208145829
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 +17 -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 -26
- 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,
|
|
@@ -256,20 +258,20 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
256
258
|
|
|
257
259
|
test('Date types edge cases mappings', async () => {
|
|
258
260
|
await connectionManager.query(`
|
|
259
|
-
INSERT INTO
|
|
261
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime2_col)
|
|
260
262
|
VALUES ('0001-01-01 00:00:00.000')
|
|
261
263
|
`);
|
|
262
264
|
await connectionManager.query(`
|
|
263
|
-
INSERT INTO
|
|
265
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime2_col)
|
|
264
266
|
VALUES ('9999-12-31 23:59:59.999')
|
|
265
267
|
`);
|
|
266
268
|
await connectionManager.query(`
|
|
267
|
-
INSERT INTO
|
|
269
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime_col)
|
|
268
270
|
VALUES ('1753-01-01 00:00:00')
|
|
269
271
|
`);
|
|
270
|
-
const beforeLSN = await
|
|
272
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
271
273
|
await connectionManager.query(`
|
|
272
|
-
INSERT INTO
|
|
274
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetime_col)
|
|
273
275
|
VALUES ('9999-12-31 23:59:59.997')
|
|
274
276
|
`);
|
|
275
277
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
@@ -291,10 +293,10 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
291
293
|
});
|
|
292
294
|
|
|
293
295
|
test('DateTimeOffset type mapping', async () => {
|
|
294
|
-
const beforeLSN = await
|
|
296
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
295
297
|
// DateTimeOffset preserves timezone information
|
|
296
298
|
await connectionManager.query(`
|
|
297
|
-
INSERT INTO
|
|
299
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(datetimeoffset_col)
|
|
298
300
|
VALUES ('2023-03-06 15:47:00.000 +05:00')
|
|
299
301
|
`);
|
|
300
302
|
await waitForPendingCDCChanges(beforeLSN, connectionManager);
|
|
@@ -312,12 +314,12 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
312
314
|
});
|
|
313
315
|
|
|
314
316
|
test('UniqueIdentifier type mapping', async () => {
|
|
315
|
-
const beforeLSN = await
|
|
317
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
316
318
|
|
|
317
319
|
const testGuid = createUpperCaseUUID();
|
|
318
320
|
await connectionManager.query(
|
|
319
321
|
`
|
|
320
|
-
INSERT INTO
|
|
322
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(uniqueidentifier_col)
|
|
321
323
|
VALUES (@guid)
|
|
322
324
|
`,
|
|
323
325
|
[{ name: 'guid', type: sql.UniqueIdentifier, value: testGuid }]
|
|
@@ -333,11 +335,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
333
335
|
});
|
|
334
336
|
|
|
335
337
|
test('JSON type mapping', async () => {
|
|
336
|
-
const beforeLSN = await
|
|
338
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
337
339
|
const expectedJSON = { name: 'John Doe', age: 30, married: true };
|
|
338
340
|
await connectionManager.query(
|
|
339
341
|
`
|
|
340
|
-
INSERT INTO
|
|
342
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(json_col)
|
|
341
343
|
VALUES (@json)
|
|
342
344
|
`,
|
|
343
345
|
[{ name: 'json', type: sql.NVarChar(sql.MAX), value: JSON.stringify(expectedJSON) }]
|
|
@@ -354,11 +356,11 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
354
356
|
});
|
|
355
357
|
|
|
356
358
|
test('XML type mapping', async () => {
|
|
357
|
-
const beforeLSN = await
|
|
359
|
+
const beforeLSN = await getLatestLSN(connectionManager);
|
|
358
360
|
const xmlData = '<root><item>value</item></root>';
|
|
359
361
|
await connectionManager.query(
|
|
360
362
|
`
|
|
361
|
-
INSERT INTO
|
|
363
|
+
INSERT INTO ${escapeIdentifier(connectionManager.schema)}.test_data(xml_col)
|
|
362
364
|
VALUES (@xml)
|
|
363
365
|
`,
|
|
364
366
|
[{ name: 'xml', type: sql.Xml, value: xmlData }]
|
|
@@ -374,7 +376,7 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
374
376
|
|
|
375
377
|
// TODO: Update test when properly converting spatial types
|
|
376
378
|
// test('Spatial types mappings', async () => {
|
|
377
|
-
// const beforeLSN = await
|
|
379
|
+
// const beforeLSN = await getLatestLSN(connectionManager);
|
|
378
380
|
// // Geometry and Geography types are stored as binary/WKT strings
|
|
379
381
|
// await connectionManager.query(`
|
|
380
382
|
// INSERT INTO [${connectionManager.schema}].test_data(geometry_col, geography_col)
|
|
@@ -398,7 +400,7 @@ describe('MSSQL Data Types Tests', () => {
|
|
|
398
400
|
// TODO: Enable when HierarchyID type is properly supported
|
|
399
401
|
// test('HierarchyID type mapping', async () => {
|
|
400
402
|
// const hierarchyid = '/1/';
|
|
401
|
-
// const beforeLSN = await
|
|
403
|
+
// const beforeLSN = await getLatestLSN(connectionManager);
|
|
402
404
|
// await connectionManager.query(`
|
|
403
405
|
// INSERT INTO [${connectionManager.schema}].test_data(hierarchyid_col)
|
|
404
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;
|