@powersync/service-module-mongodb 0.13.2 → 0.15.0

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.
@@ -3,12 +3,11 @@ import { setTimeout } from 'node:timers/promises';
3
3
  import { describe, expect, test, vi } from 'vitest';
4
4
 
5
5
  import { mongo } from '@powersync/lib-service-mongodb';
6
- import { storage } from '@powersync/service-core';
7
6
  import { test_utils } from '@powersync/service-core-tests';
8
7
 
9
8
  import { PostImagesOption } from '@module/types/types.js';
10
9
  import { ChangeStreamTestContext } from './change_stream_utils.js';
11
- import { describeWithStorage } from './util.js';
10
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
12
11
 
13
12
  const BASIC_SYNC_RULES = `
14
13
  bucket_definitions:
@@ -21,9 +20,12 @@ describe('change stream', () => {
21
20
  describeWithStorage({ timeout: 20_000 }, defineChangeStreamTests);
22
21
  });
23
22
 
24
- function defineChangeStreamTests(factory: storage.TestStorageFactory) {
23
+ function defineChangeStreamTests({ factory, storageVersion }: StorageVersionTestContext) {
24
+ const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
25
+ return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
26
+ };
25
27
  test('replicating basic values', async () => {
26
- await using context = await ChangeStreamTestContext.open(factory, {
28
+ await using context = await openContext({
27
29
  mongoOptions: { postImages: PostImagesOption.READ_ONLY }
28
30
  });
29
31
  const { db } = context;
@@ -59,7 +61,7 @@ bucket_definitions:
59
61
  });
60
62
 
61
63
  test('replicating wildcard', async () => {
62
- await using context = await ChangeStreamTestContext.open(factory);
64
+ await using context = await openContext();
63
65
  const { db } = context;
64
66
  await context.updateSyncRules(`
65
67
  bucket_definitions:
@@ -91,7 +93,7 @@ bucket_definitions:
91
93
  });
92
94
 
93
95
  test('updateLookup - no fullDocument available', async () => {
94
- await using context = await ChangeStreamTestContext.open(factory, {
96
+ await using context = await openContext({
95
97
  mongoOptions: { postImages: PostImagesOption.OFF }
96
98
  });
97
99
  const { db, client } = context;
@@ -137,7 +139,7 @@ bucket_definitions:
137
139
  test('postImages - autoConfigure', async () => {
138
140
  // Similar to the above test, but with postImages enabled.
139
141
  // This resolves the consistency issue.
140
- await using context = await ChangeStreamTestContext.open(factory, {
142
+ await using context = await openContext({
141
143
  mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
142
144
  });
143
145
  const { db, client } = context;
@@ -185,7 +187,7 @@ bucket_definitions:
185
187
  test('postImages - on', async () => {
186
188
  // Similar to postImages - autoConfigure, but does not auto-configure.
187
189
  // changeStreamPreAndPostImages must be manually configured.
188
- await using context = await ChangeStreamTestContext.open(factory, {
190
+ await using context = await openContext({
189
191
  mongoOptions: { postImages: PostImagesOption.READ_ONLY }
190
192
  });
191
193
  const { db, client } = context;
@@ -230,7 +232,7 @@ bucket_definitions:
230
232
  });
231
233
 
232
234
  test('replicating case sensitive table', async () => {
233
- await using context = await ChangeStreamTestContext.open(factory);
235
+ await using context = await openContext();
234
236
  const { db } = context;
235
237
  await context.updateSyncRules(`
236
238
  bucket_definitions:
@@ -254,7 +256,7 @@ bucket_definitions:
254
256
  });
255
257
 
256
258
  test('replicating large values', async () => {
257
- await using context = await ChangeStreamTestContext.open(factory);
259
+ await using context = await openContext();
258
260
  const { db } = context;
259
261
  await context.updateSyncRules(`
260
262
  bucket_definitions:
@@ -285,7 +287,7 @@ bucket_definitions:
285
287
  });
286
288
 
287
289
  test('replicating dropCollection', async () => {
288
- await using context = await ChangeStreamTestContext.open(factory);
290
+ await using context = await openContext();
289
291
  const { db } = context;
290
292
  const syncRuleContent = `
291
293
  bucket_definitions:
@@ -317,7 +319,7 @@ bucket_definitions:
317
319
  });
318
320
 
319
321
  test('replicating renameCollection', async () => {
320
- await using context = await ChangeStreamTestContext.open(factory);
322
+ await using context = await openContext();
321
323
  const { db } = context;
322
324
  const syncRuleContent = `
323
325
  bucket_definitions:
@@ -348,7 +350,7 @@ bucket_definitions:
348
350
  });
349
351
 
350
352
  test('initial sync', async () => {
351
- await using context = await ChangeStreamTestContext.open(factory);
353
+ await using context = await openContext();
352
354
  const { db } = context;
353
355
  await context.updateSyncRules(BASIC_SYNC_RULES);
354
356
 
@@ -373,7 +375,7 @@ bucket_definitions:
373
375
  // MongoServerError: PlanExecutor error during aggregation :: caused by :: BSONObj size: 33554925 (0x20001ED) is invalid.
374
376
  // Size must be between 0 and 16793600(16MB)
375
377
 
376
- await using context = await ChangeStreamTestContext.open(factory);
378
+ await using context = await openContext();
377
379
  await context.updateSyncRules(`bucket_definitions:
378
380
  global:
379
381
  data:
@@ -422,7 +424,7 @@ bucket_definitions:
422
424
  });
423
425
 
424
426
  test('collection not in sync rules', async () => {
425
- await using context = await ChangeStreamTestContext.open(factory);
427
+ await using context = await openContext();
426
428
  const { db } = context;
427
429
  await context.updateSyncRules(BASIC_SYNC_RULES);
428
430
 
@@ -439,7 +441,7 @@ bucket_definitions:
439
441
  });
440
442
 
441
443
  test('postImages - new collection with postImages enabled', async () => {
442
- await using context = await ChangeStreamTestContext.open(factory, {
444
+ await using context = await openContext({
443
445
  mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
444
446
  });
445
447
  const { db } = context;
@@ -472,7 +474,7 @@ bucket_definitions:
472
474
  });
473
475
 
474
476
  test('postImages - new collection with postImages disabled', async () => {
475
- await using context = await ChangeStreamTestContext.open(factory, {
477
+ await using context = await openContext({
476
478
  mongoOptions: { postImages: PostImagesOption.AUTO_CONFIGURE }
477
479
  });
478
480
  const { db } = context;
@@ -502,7 +504,7 @@ bucket_definitions:
502
504
  });
503
505
 
504
506
  test('recover from error', async () => {
505
- await using context = await ChangeStreamTestContext.open(factory);
507
+ await using context = await openContext();
506
508
  const { db } = context;
507
509
  await context.updateSyncRules(`
508
510
  bucket_definitions:
@@ -4,13 +4,18 @@ import {
4
4
  createCoreReplicationMetrics,
5
5
  initializeCoreReplicationMetrics,
6
6
  InternalOpId,
7
+ LEGACY_STORAGE_VERSION,
7
8
  OplogEntry,
8
9
  ProtocolOpId,
9
10
  ReplicationCheckpoint,
11
+ storage,
12
+ STORAGE_VERSION_CONFIG,
10
13
  SyncRulesBucketStorage,
11
- TestStorageOptions
14
+ TestStorageOptions,
15
+ updateSyncRulesFromYaml,
16
+ utils
12
17
  } from '@powersync/service-core';
13
- import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
18
+ import { bucketRequest, METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
14
19
 
15
20
  import { ChangeStream, ChangeStreamOptions } from '@module/replication/ChangeStream.js';
16
21
  import { MongoManager } from '@module/replication/MongoManager.js';
@@ -22,7 +27,9 @@ import { clearTestDb, TEST_CONNECTION_OPTIONS } from './util.js';
22
27
  export class ChangeStreamTestContext {
23
28
  private _walStream?: ChangeStream;
24
29
  private abortController = new AbortController();
25
- private streamPromise?: Promise<void>;
30
+ private streamPromise?: Promise<PromiseSettledResult<void>>;
31
+ private syncRulesId?: number;
32
+ private syncRulesContent?: storage.PersistedSyncRulesContent;
26
33
  public storage?: SyncRulesBucketStorage;
27
34
 
28
35
  /**
@@ -35,6 +42,7 @@ export class ChangeStreamTestContext {
35
42
  factory: (options: TestStorageOptions) => Promise<BucketStorageFactory>,
36
43
  options?: {
37
44
  doNotClear?: boolean;
45
+ storageVersion?: number;
38
46
  mongoOptions?: Partial<NormalizedMongoConnectionConfig>;
39
47
  streamOptions?: Partial<ChangeStreamOptions>;
40
48
  }
@@ -45,13 +53,19 @@ export class ChangeStreamTestContext {
45
53
  if (!options?.doNotClear) {
46
54
  await clearTestDb(connectionManager.db);
47
55
  }
48
- return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions);
56
+
57
+ const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
58
+ const versionedBuckets = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false;
59
+
60
+ return new ChangeStreamTestContext(f, connectionManager, options?.streamOptions, storageVersion, versionedBuckets);
49
61
  }
50
62
 
51
63
  constructor(
52
64
  public factory: BucketStorageFactory,
53
65
  public connectionManager: MongoManager,
54
- private streamOptions?: Partial<ChangeStreamOptions>
66
+ private streamOptions: Partial<ChangeStreamOptions> = {},
67
+ private storageVersion: number = LEGACY_STORAGE_VERSION,
68
+ private versionedBuckets: boolean = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false
55
69
  ) {
56
70
  createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
57
71
  initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
@@ -88,7 +102,11 @@ export class ChangeStreamTestContext {
88
102
  }
89
103
 
90
104
  async updateSyncRules(content: string) {
91
- const syncRules = await this.factory.updateSyncRules({ content: content, validate: true });
105
+ const syncRules = await this.factory.updateSyncRules(
106
+ updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
107
+ );
108
+ this.syncRulesId = syncRules.id;
109
+ this.syncRulesContent = syncRules;
92
110
  this.storage = this.factory.getInstance(syncRules);
93
111
  return this.storage!;
94
112
  }
@@ -99,11 +117,20 @@ export class ChangeStreamTestContext {
99
117
  throw new Error(`Next sync rules not available`);
100
118
  }
101
119
 
120
+ this.syncRulesId = syncRules.id;
121
+ this.syncRulesContent = syncRules;
102
122
  this.storage = this.factory.getInstance(syncRules);
103
123
  return this.storage!;
104
124
  }
105
125
 
106
- get walStream() {
126
+ private getSyncRulesContent(): storage.PersistedSyncRulesContent {
127
+ if (this.syncRulesContent == null) {
128
+ throw new Error('Sync rules not configured - call updateSyncRules() first');
129
+ }
130
+ return this.syncRulesContent;
131
+ }
132
+
133
+ get streamer() {
107
134
  if (this.storage == null) {
108
135
  throw new Error('updateSyncRules() first');
109
136
  }
@@ -125,7 +152,7 @@ export class ChangeStreamTestContext {
125
152
  }
126
153
 
127
154
  async replicateSnapshot() {
128
- await this.walStream.initReplication();
155
+ await this.streamer.initReplication();
129
156
  }
130
157
 
131
158
  /**
@@ -143,13 +170,21 @@ export class ChangeStreamTestContext {
143
170
  }
144
171
 
145
172
  startStreaming() {
146
- return (this.streamPromise = this.walStream.streamChanges());
173
+ this.streamPromise = this.streamer
174
+ .streamChanges()
175
+ .then(() => ({ status: 'fulfilled', value: undefined }) satisfies PromiseFulfilledResult<void>)
176
+ .catch((reason) => ({ status: 'rejected', reason }) satisfies PromiseRejectedResult);
177
+ return this.streamPromise;
147
178
  }
148
179
 
149
180
  async getCheckpoint(options?: { timeout?: number }) {
150
181
  let checkpoint = await Promise.race([
151
182
  getClientCheckpoint(this.client, this.db, this.factory, { timeout: options?.timeout ?? 15_000 }),
152
- this.streamPromise
183
+ this.streamPromise?.then((e) => {
184
+ if (e.status == 'rejected') {
185
+ throw e.reason;
186
+ }
187
+ })
153
188
  ]);
154
189
  if (checkpoint == null) {
155
190
  // This indicates an issue with the test setup - streamingPromise completed instead
@@ -161,7 +196,8 @@ export class ChangeStreamTestContext {
161
196
 
162
197
  async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
163
198
  let checkpoint = await this.getCheckpoint(options);
164
- const map = new Map<string, InternalOpId>(Object.entries(buckets));
199
+ const syncRules = this.getSyncRulesContent();
200
+ const map = Object.entries(buckets).map(([bucket, start]) => bucketRequest(syncRules, bucket, start));
165
201
  return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
166
202
  }
167
203
 
@@ -170,8 +206,9 @@ export class ChangeStreamTestContext {
170
206
  if (typeof start == 'string') {
171
207
  start = BigInt(start);
172
208
  }
209
+ const syncRules = this.getSyncRulesContent();
173
210
  const checkpoint = await this.getCheckpoint(options);
174
- const map = new Map<string, InternalOpId>([[bucket, start]]);
211
+ let map = [bucketRequest(syncRules, bucket, start)];
175
212
  let data: OplogEntry[] = [];
176
213
  while (true) {
177
214
  const batch = this.storage!.getBucketDataBatch(checkpoint, map);
@@ -181,20 +218,27 @@ export class ChangeStreamTestContext {
181
218
  if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
182
219
  break;
183
220
  }
184
- map.set(bucket, BigInt(batches[0]!.chunkData.next_after));
221
+ map = [bucketRequest(syncRules, bucket, BigInt(batches[0]!.chunkData.next_after))];
185
222
  }
186
223
  return data;
187
224
  }
188
225
 
189
- async getChecksums(buckets: string[], options?: { timeout?: number }) {
226
+ async getChecksums(buckets: string[], options?: { timeout?: number }): Promise<utils.ChecksumMap> {
190
227
  let checkpoint = await this.getCheckpoint(options);
191
- return this.storage!.getChecksums(checkpoint, buckets);
228
+ const syncRules = this.getSyncRulesContent();
229
+ const versionedBuckets = buckets.map((bucket) => bucketRequest(syncRules, bucket, 0n));
230
+ const checksums = await this.storage!.getChecksums(checkpoint, versionedBuckets);
231
+
232
+ const unversioned: utils.ChecksumMap = new Map();
233
+ for (let i = 0; i < buckets.length; i++) {
234
+ unversioned.set(buckets[i], checksums.get(versionedBuckets[i].bucket)!);
235
+ }
236
+ return unversioned;
192
237
  }
193
238
 
194
239
  async getChecksum(bucket: string, options?: { timeout?: number }) {
195
- let checkpoint = await this.getCheckpoint(options);
196
- const map = await this.storage!.getChecksums(checkpoint, [bucket]);
197
- return map.get(bucket);
240
+ const checksums = await this.getChecksums([bucket], options);
241
+ return checksums.get(bucket);
198
242
  }
199
243
  }
200
244
 
@@ -1,18 +1,22 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { reduceBucket, TestStorageFactory } from '@powersync/service-core';
2
+ import { reduceBucket } from '@powersync/service-core';
3
3
  import { METRICS_HELPER } from '@powersync/service-core-tests';
4
4
  import { JSONBig } from '@powersync/service-jsonbig';
5
5
  import { SqliteJsonValue } from '@powersync/service-sync-rules';
6
6
  import * as timers from 'timers/promises';
7
7
  import { describe, expect, test } from 'vitest';
8
8
  import { ChangeStreamTestContext } from './change_stream_utils.js';
9
- import { describeWithStorage } from './util.js';
9
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
10
10
 
11
11
  describe('chunked snapshots', () => {
12
12
  describeWithStorage({ timeout: 120_000 }, defineBatchTests);
13
13
  });
14
14
 
15
- function defineBatchTests(factory: TestStorageFactory) {
15
+ function defineBatchTests({ factory, storageVersion }: StorageVersionTestContext) {
16
+ const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
17
+ return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
18
+ };
19
+
16
20
  // This is not as sensitive to the id type as postgres, but we still test a couple of cases
17
21
  test('chunked snapshot (int32)', async () => {
18
22
  await testChunkedSnapshot({
@@ -93,7 +97,7 @@ function defineBatchTests(factory: TestStorageFactory) {
93
97
  const idToSqlite = options.idToSqlite ?? ((n) => n);
94
98
  const idToString = (id: any) => String(idToSqlite(id));
95
99
 
96
- await using context = await ChangeStreamTestContext.open(factory, {
100
+ await using context = await openContext({
97
101
  // We need to use a smaller chunk size here, so that we can run a query in between chunks
98
102
  streamOptions: { snapshotChunkLength: 100 }
99
103
  });
@@ -1,19 +1,22 @@
1
1
  import { ChangeStreamInvalidatedError } from '@module/replication/ChangeStream.js';
2
2
  import { MongoManager } from '@module/replication/MongoManager.js';
3
3
  import { normalizeConnectionConfig } from '@module/types/types.js';
4
- import { BucketStorageFactory, TestStorageOptions } from '@powersync/service-core';
5
4
  import { describe, expect, test } from 'vitest';
6
5
  import { ChangeStreamTestContext } from './change_stream_utils.js';
7
6
  import { env } from './env.js';
8
- import { describeWithStorage } from './util.js';
7
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
9
8
 
10
9
  describe('mongodb resuming replication', () => {
11
10
  describeWithStorage({}, defineResumeTest);
12
11
  });
13
12
 
14
- function defineResumeTest(factoryGenerator: (options?: TestStorageOptions) => Promise<BucketStorageFactory>) {
13
+ function defineResumeTest({ factory: factoryGenerator, storageVersion }: StorageVersionTestContext) {
14
+ const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
15
+ return ChangeStreamTestContext.open(factoryGenerator, { ...options, storageVersion });
16
+ };
17
+
15
18
  test('resuming with a different source database', async () => {
16
- await using context = await ChangeStreamTestContext.open(factoryGenerator);
19
+ await using context = await openContext();
17
20
  const { db } = context;
18
21
 
19
22
  await context.updateSyncRules(/* yaml */
@@ -53,13 +56,14 @@ function defineResumeTest(factoryGenerator: (options?: TestStorageOptions) => Pr
53
56
  const factory = await factoryGenerator({ doNotClear: true });
54
57
 
55
58
  // Create a new context without updating the sync rules
56
- await using context2 = new ChangeStreamTestContext(factory, connectionManager);
59
+ await using context2 = new ChangeStreamTestContext(factory, connectionManager, {}, storageVersion);
57
60
  const activeContent = await factory.getActiveSyncRulesContent();
58
61
  context2.storage = factory.getInstance(activeContent!);
59
62
 
60
63
  // If this test times out, it likely didn't throw the expected error here.
61
- const error = await context2.startStreaming().catch((ex) => ex);
64
+ const result = await context2.startStreaming();
62
65
  // The ChangeStreamReplicationJob will detect this and throw a ChangeStreamInvalidatedError
63
- expect(error).toBeInstanceOf(ChangeStreamInvalidatedError);
66
+ expect(result.status).toEqual('rejected');
67
+ expect((result as PromiseRejectedResult).reason).toBeInstanceOf(ChangeStreamInvalidatedError);
64
68
  });
65
69
  }
@@ -8,19 +8,19 @@ import { env } from './env.js';
8
8
  import { describeWithStorage } from './util.js';
9
9
 
10
10
  describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
11
- describeWithStorage({ timeout: 240_000 }, function (factory) {
11
+ describeWithStorage({ timeout: 240_000 }, function ({ factory, storageVersion }) {
12
12
  test('resuming initial replication (1)', async () => {
13
13
  // Stop early - likely to not include deleted row in first replication attempt.
14
- await testResumingReplication(factory, 2000);
14
+ await testResumingReplication(factory, storageVersion, 2000);
15
15
  });
16
16
  test('resuming initial replication (2)', async () => {
17
17
  // Stop late - likely to include deleted row in first replication attempt.
18
- await testResumingReplication(factory, 8000);
18
+ await testResumingReplication(factory, storageVersion, 8000);
19
19
  });
20
20
  });
21
21
  });
22
22
 
23
- async function testResumingReplication(factory: TestStorageFactory, stopAfter: number) {
23
+ async function testResumingReplication(factory: TestStorageFactory, storageVersion: number, stopAfter: number) {
24
24
  // This tests interrupting and then resuming initial replication.
25
25
  // We interrupt replication after test_data1 has fully replicated, and
26
26
  // test_data2 has partially replicated.
@@ -35,7 +35,10 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
35
35
  let startRowCount: number;
36
36
 
37
37
  {
38
- await using context = await ChangeStreamTestContext.open(factory, { streamOptions: { snapshotChunkLength: 1000 } });
38
+ await using context = await ChangeStreamTestContext.open(factory, {
39
+ storageVersion,
40
+ streamOptions: { snapshotChunkLength: 1000 }
41
+ });
39
42
 
40
43
  await context.updateSyncRules(`bucket_definitions:
41
44
  global:
@@ -87,6 +90,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
87
90
  // Bypass the usual "clear db on factory open" step.
88
91
  await using context2 = await ChangeStreamTestContext.open(factory, {
89
92
  doNotClear: true,
93
+ storageVersion,
90
94
  streamOptions: { snapshotChunkLength: 1000 }
91
95
  });
92
96
 
@@ -2,19 +2,21 @@ import { setTimeout } from 'node:timers/promises';
2
2
  import { describe, expect, test } from 'vitest';
3
3
 
4
4
  import { mongo } from '@powersync/lib-service-mongodb';
5
- import { storage } from '@powersync/service-core';
6
-
7
5
  import { ChangeStreamTestContext, setSnapshotHistorySeconds } from './change_stream_utils.js';
8
6
  import { env } from './env.js';
9
- import { describeWithStorage } from './util.js';
7
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
10
8
 
11
9
  describe.runIf(env.CI || env.SLOW_TESTS)('change stream slow tests', { timeout: 60_000 }, function () {
12
10
  describeWithStorage({}, defineSlowTests);
13
11
  });
14
12
 
15
- function defineSlowTests(factory: storage.TestStorageFactory) {
13
+ function defineSlowTests({ factory, storageVersion }: StorageVersionTestContext) {
14
+ const openContext = (options?: Parameters<typeof ChangeStreamTestContext.open>[1]) => {
15
+ return ChangeStreamTestContext.open(factory, { ...options, storageVersion });
16
+ };
17
+
16
18
  test('replicating snapshot with lots of data', async () => {
17
- await using context = await ChangeStreamTestContext.open(factory);
19
+ await using context = await openContext();
18
20
  // Test with low minSnapshotHistoryWindowInSeconds, to trigger:
19
21
  // > Read timestamp .. is older than the oldest available timestamp.
20
22
  // This happened when we had {snapshot: true} in the initial
@@ -52,7 +54,7 @@ bucket_definitions:
52
54
  // changestream), we may miss updates, which this test would
53
55
  // hopefully catch.
54
56
 
55
- await using context = await ChangeStreamTestContext.open(factory);
57
+ await using context = await openContext();
56
58
  const { db } = context;
57
59
  await context.updateSyncRules(`
58
60
  bucket_definitions:
package/test/src/util.ts CHANGED
@@ -3,9 +3,14 @@ import * as mongo_storage from '@powersync/service-module-mongodb-storage';
3
3
  import * as postgres_storage from '@powersync/service-module-postgres-storage';
4
4
 
5
5
  import * as types from '@module/types/types.js';
6
- import { env } from './env.js';
7
- import { BSON_DESERIALIZE_DATA_OPTIONS, TestStorageFactory } from '@powersync/service-core';
6
+ import {
7
+ BSON_DESERIALIZE_DATA_OPTIONS,
8
+ SUPPORTED_STORAGE_VERSIONS,
9
+ TestStorageConfig,
10
+ TestStorageFactory
11
+ } from '@powersync/service-core';
8
12
  import { describe, TestOptions } from 'vitest';
13
+ import { env } from './env.js';
9
14
 
10
15
  export const TEST_URI = env.MONGO_TEST_DATA_URL;
11
16
 
@@ -19,18 +24,38 @@ export const INITIALIZED_MONGO_STORAGE_FACTORY = mongo_storage.test_utils.mongoT
19
24
  isCI: env.CI
20
25
  });
21
26
 
22
- export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.postgresTestStorageFactoryGenerator({
27
+ export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.postgresTestSetup({
23
28
  url: env.PG_STORAGE_TEST_URL
24
29
  });
25
30
 
26
- export function describeWithStorage(options: TestOptions, fn: (factory: TestStorageFactory) => void) {
27
- describe.skipIf(!env.TEST_MONGO_STORAGE)(`mongodb storage`, options, function () {
28
- fn(INITIALIZED_MONGO_STORAGE_FACTORY);
29
- });
31
+ export const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
30
32
 
31
- describe.skipIf(!env.TEST_POSTGRES_STORAGE)(`postgres storage`, options, function () {
32
- fn(INITIALIZED_POSTGRES_STORAGE_FACTORY);
33
- });
33
+ export interface StorageVersionTestContext {
34
+ factory: TestStorageFactory;
35
+ storageVersion: number;
36
+ }
37
+
38
+ export function describeWithStorage(options: TestOptions, fn: (context: StorageVersionTestContext) => void) {
39
+ const describeFactory = (storageName: string, config: TestStorageConfig) => {
40
+ describe(`${storageName} storage`, options, function () {
41
+ for (const storageVersion of TEST_STORAGE_VERSIONS) {
42
+ describe(`storage v${storageVersion}`, function () {
43
+ fn({
44
+ factory: config.factory,
45
+ storageVersion
46
+ });
47
+ });
48
+ }
49
+ });
50
+ };
51
+
52
+ if (env.TEST_MONGO_STORAGE) {
53
+ describeFactory('mongodb', INITIALIZED_MONGO_STORAGE_FACTORY);
54
+ }
55
+
56
+ if (env.TEST_POSTGRES_STORAGE) {
57
+ describeFactory('postgres', INITIALIZED_POSTGRES_STORAGE_FACTORY);
58
+ }
34
59
  }
35
60
 
36
61
  export async function clearTestDb(db: mongo.Db) {