@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +17 -12
  2. package/dist/api/MSSQLRouteAPIAdapter.js +15 -50
  3. package/dist/api/MSSQLRouteAPIAdapter.js.map +1 -1
  4. package/dist/common/LSN.js +2 -2
  5. package/dist/common/LSN.js.map +1 -1
  6. package/dist/common/MSSQLSourceTable.js +4 -4
  7. package/dist/common/MSSQLSourceTable.js.map +1 -1
  8. package/dist/common/mssqls-to-sqlite.js +4 -4
  9. package/dist/common/mssqls-to-sqlite.js.map +1 -1
  10. package/dist/replication/CDCPoller.js +1 -2
  11. package/dist/replication/CDCPoller.js.map +1 -1
  12. package/dist/replication/CDCStream.js +24 -32
  13. package/dist/replication/CDCStream.js.map +1 -1
  14. package/dist/replication/MSSQLSnapshotQuery.d.ts +0 -17
  15. package/dist/replication/MSSQLSnapshotQuery.js +0 -47
  16. package/dist/replication/MSSQLSnapshotQuery.js.map +1 -1
  17. package/dist/utils/mssql.js +27 -13
  18. package/dist/utils/mssql.js.map +1 -1
  19. package/dist/utils/schema.js +31 -15
  20. package/dist/utils/schema.js.map +1 -1
  21. package/package.json +9 -9
  22. package/src/api/MSSQLRouteAPIAdapter.ts +15 -50
  23. package/src/common/LSN.ts +2 -2
  24. package/src/common/MSSQLSourceTable.ts +4 -4
  25. package/src/common/mssqls-to-sqlite.ts +11 -4
  26. package/src/replication/CDCPoller.ts +1 -2
  27. package/src/replication/CDCStream.ts +26 -42
  28. package/src/replication/MSSQLSnapshotQuery.ts +0 -54
  29. package/src/utils/mssql.ts +28 -15
  30. package/src/utils/schema.ts +31 -17
  31. package/test/src/CDCStream.test.ts +5 -5
  32. package/test/src/CDCStream_resumable_snapshot.test.ts +3 -3
  33. package/test/src/mssql-to-sqlite.test.ts +28 -26
  34. package/test/src/util.ts +16 -6
  35. package/tsconfig.tsbuildinfo +1 -1
  36. 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
- }
@@ -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 = '${connectionManager.schema}' AND TABLE_NAME = '${POWERSYNC_CHECKPOINTS_TABLE}';
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 = '${schema}'
168
- AND tbl.name = '${table}'
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('${captureInstance}') AS 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 = '${schema}'
262
- AND tbl.name = '${tableName}'
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
- return `[${identifier}]`;
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 ${toQualifiedTableName(schema, table.name)}`);
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
  }
@@ -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 { escapeIdentifier } from './mssql.js';
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 = '${schema}'
27
- AND tbl.name = '${tableName}'
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 = '${schema}'
69
- AND tbl.name = '${tableName}'
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 = '${schema}'
99
- AND tbl.name = '${tableName}'
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 = '${tablePattern.schema}'
138
- AND tbl.name LIKE '${tablePattern.tablePattern}'
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
- WHERE sch.name = '${tablePattern.schema}'
160
- AND tbl.name = '${tablePattern.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 { getLatestReplicatedLSN } from '@module/utils/mssql.js';
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 getLatestReplicatedLSN(connectionManager);
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 getLatestReplicatedLSN(connectionManager);
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 getLatestReplicatedLSN(connectionManager);
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 { getLatestReplicatedLSN } from '@module/utils/mssql.js';
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 getLatestReplicatedLSN(connectionManager);
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 getLatestReplicatedLSN(context2.connectionManager);
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 getLatestReplicatedLSN(connectionManager);
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: 1
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 getLatestReplicatedLSN(connectionManager);
130
+ const beforeLSN = await getLatestLSN(connectionManager);
129
131
  await connectionManager.query(`
130
- INSERT INTO [${connectionManager.schema}].test_data (
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 getLatestReplicatedLSN(connectionManager);
172
+ const beforeLSN = await getLatestLSN(connectionManager);
171
173
  const binaryData = Buffer.from('BinaryData');
172
174
  await connectionManager.query(
173
175
  `
174
- INSERT INTO [${connectionManager.schema}].test_data (
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 getLatestReplicatedLSN(connectionManager);
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 [${connectionManager.schema}].test_data(
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 [${connectionManager.schema}].test_data(datetime2_col)
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 [${connectionManager.schema}].test_data(datetime2_col)
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 [${connectionManager.schema}].test_data(datetime_col)
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 getLatestReplicatedLSN(connectionManager);
272
+ const beforeLSN = await getLatestLSN(connectionManager);
271
273
  await connectionManager.query(`
272
- INSERT INTO [${connectionManager.schema}].test_data(datetime_col)
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 getLatestReplicatedLSN(connectionManager);
296
+ const beforeLSN = await getLatestLSN(connectionManager);
295
297
  // DateTimeOffset preserves timezone information
296
298
  await connectionManager.query(`
297
- INSERT INTO [${connectionManager.schema}].test_data(datetimeoffset_col)
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 getLatestReplicatedLSN(connectionManager);
317
+ const beforeLSN = await getLatestLSN(connectionManager);
316
318
 
317
319
  const testGuid = createUpperCaseUUID();
318
320
  await connectionManager.query(
319
321
  `
320
- INSERT INTO [${connectionManager.schema}].test_data(uniqueidentifier_col)
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 getLatestReplicatedLSN(connectionManager);
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 [${connectionManager.schema}].test_data(json_col)
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 getLatestReplicatedLSN(connectionManager);
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 [${connectionManager.schema}].test_data(xml_col)
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 getLatestReplicatedLSN(connectionManager);
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 getLatestReplicatedLSN(connectionManager);
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 500ms...`);
145
- await new Promise((resolve) => setTimeout(resolve, 500));
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;