@powersync/service-module-postgres 0.17.2 → 0.19.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.
@@ -7,6 +7,7 @@ import {
7
7
  connectPgPool,
8
8
  describeWithStorage,
9
9
  getClientCheckpoint,
10
+ StorageVersionTestContext,
10
11
  TEST_CONNECTION_OPTIONS
11
12
  } from './util.js';
12
13
 
@@ -14,20 +15,26 @@ import * as pgwire from '@powersync/service-jpgwire';
14
15
  import { SqliteRow } from '@powersync/service-sync-rules';
15
16
 
16
17
  import { PgManager } from '@module/replication/PgManager.js';
17
- import { createCoreReplicationMetrics, initializeCoreReplicationMetrics, storage } from '@powersync/service-core';
18
+ import { ReplicationAbortedError } from '@powersync/lib-services-framework';
19
+ import {
20
+ createCoreReplicationMetrics,
21
+ initializeCoreReplicationMetrics,
22
+ reduceBucket,
23
+ updateSyncRulesFromYaml
24
+ } from '@powersync/service-core';
18
25
  import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
19
26
  import * as mongo_storage from '@powersync/service-module-mongodb-storage';
20
27
  import * as postgres_storage from '@powersync/service-module-postgres-storage';
21
28
  import * as timers from 'node:timers/promises';
22
- import { CustomTypeRegistry } from '@module/types/registry.js';
29
+ import { WalStreamTestContext } from './wal_stream_utils.js';
23
30
 
24
31
  describe.skipIf(!(env.CI || env.SLOW_TESTS))('slow tests', function () {
25
- describeWithStorage({ timeout: 120_000 }, function (factory) {
26
- defineSlowTests(factory);
32
+ describeWithStorage({ timeout: 120_000 }, function ({ factory, storageVersion }) {
33
+ defineSlowTests({ factory, storageVersion });
27
34
  });
28
35
  });
29
36
 
30
- function defineSlowTests(factory: storage.TestStorageFactory) {
37
+ function defineSlowTests({ factory, storageVersion }: StorageVersionTestContext) {
31
38
  let walStream: WalStream | undefined;
32
39
  let connections: PgManager | undefined;
33
40
  let abortController: AbortController | undefined;
@@ -42,7 +49,7 @@ function defineSlowTests(factory: storage.TestStorageFactory) {
42
49
  // This cleans up, similar to WalStreamTestContext.dispose().
43
50
  // These tests are a little more complex than what is supported by WalStreamTestContext.
44
51
  abortController?.abort();
45
- await streamPromise;
52
+ await streamPromise?.catch((_) => {});
46
53
  streamPromise = undefined;
47
54
  connections?.destroy();
48
55
 
@@ -70,7 +77,6 @@ function defineSlowTests(factory: storage.TestStorageFactory) {
70
77
 
71
78
  async function testRepeatedReplication(testOptions: { compact: boolean; maxBatchSize: number; numBatches: number }) {
72
79
  const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
73
- const replicationConnection = await connections.replicationConnection();
74
80
  const pool = connections.pool;
75
81
  await clearTestDb(pool);
76
82
  await using f = await factory();
@@ -81,7 +87,7 @@ bucket_definitions:
81
87
  data:
82
88
  - SELECT * FROM "test_data"
83
89
  `;
84
- const syncRules = await f.updateSyncRules({ content: syncRuleContent });
90
+ const syncRules = await f.updateSyncRules(updateSyncRulesFromYaml(syncRuleContent, { storageVersion }));
85
91
  const storage = f.getInstance(syncRules);
86
92
  abortController = new AbortController();
87
93
  const options: WalStreamOptions = {
@@ -97,11 +103,11 @@ bucket_definitions:
97
103
  );
98
104
  await pool.query(`ALTER TABLE test_data REPLICA IDENTITY FULL`);
99
105
 
100
- await walStream.initReplication(replicationConnection);
101
106
  let abort = false;
102
- streamPromise = walStream.streamChanges(replicationConnection).finally(() => {
107
+ streamPromise = walStream.replicate().finally(() => {
103
108
  abort = true;
104
109
  });
110
+ await walStream.waitForInitialSnapshot();
105
111
  const start = Date.now();
106
112
 
107
113
  while (!abort && Date.now() - start < TEST_DURATION_MS) {
@@ -223,11 +229,12 @@ bucket_definitions:
223
229
  await compactPromise;
224
230
 
225
231
  // Wait for replication to finish
226
- let checkpoint = await getClientCheckpoint(pool, storage.factory, { timeout: TIMEOUT_MARGIN_MS });
232
+ await getClientCheckpoint(pool, storage.factory, { timeout: TIMEOUT_MARGIN_MS });
227
233
 
228
234
  if (f instanceof mongo_storage.storage.MongoBucketStorage) {
229
235
  // Check that all inserts have been deleted again
230
- const docs = await f.db.current_data.find().toArray();
236
+ // Note: at this point, the pending_delete cleanup may not have run yet.
237
+ const docs = await f.db.current_data.find({ pending_delete: { $exists: false } }).toArray();
231
238
  const transformed = docs.map((doc) => {
232
239
  return bson.deserialize(doc.data.buffer) as SqliteRow;
233
240
  });
@@ -249,13 +256,14 @@ bucket_definitions:
249
256
  } else if (f instanceof postgres_storage.storage.PostgresBucketStorageFactory) {
250
257
  const { db } = f;
251
258
  // Check that all inserts have been deleted again
259
+ // FIXME: handle different storage versions
252
260
  const docs = await db.sql`
253
261
  SELECT
254
262
  *
255
263
  FROM
256
264
  current_data
257
265
  `
258
- .decoded(postgres_storage.models.CurrentData)
266
+ .decoded(postgres_storage.models.V1CurrentData)
259
267
  .rows();
260
268
  const transformed = docs.map((doc) => {
261
269
  return bson.deserialize(doc.data) as SqliteRow;
@@ -288,14 +296,20 @@ bucket_definitions:
288
296
  }
289
297
 
290
298
  abortController.abort();
291
- await streamPromise;
299
+ await streamPromise.catch((e) => {
300
+ if (e instanceof ReplicationAbortedError) {
301
+ // Ignore
302
+ } else {
303
+ throw e;
304
+ }
305
+ });
292
306
  }
293
307
 
294
308
  // Test repeatedly performing initial replication.
295
309
  //
296
310
  // If the first LSN does not correctly match with the first replication transaction,
297
311
  // we may miss some updates.
298
- test('repeated initial replication', { timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }, async () => {
312
+ test('repeated initial replication (1)', { timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }, async () => {
299
313
  const pool = await connectPgPool();
300
314
  await clearTestDb(pool);
301
315
  await using f = await factory();
@@ -306,7 +320,8 @@ bucket_definitions:
306
320
  data:
307
321
  - SELECT id, description FROM "test_data"
308
322
  `;
309
- const syncRules = await f.updateSyncRules({ content: syncRuleContent });
323
+
324
+ const syncRules = await f.updateSyncRules(updateSyncRulesFromYaml(syncRuleContent, { storageVersion }));
310
325
  const storage = f.getInstance(syncRules);
311
326
 
312
327
  // 1. Setup some base data that will be replicated in initial replication
@@ -331,7 +346,6 @@ bucket_definitions:
331
346
  i += 1;
332
347
 
333
348
  const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
334
- const replicationConnection = await connections.replicationConnection();
335
349
 
336
350
  abortController = new AbortController();
337
351
  const options: WalStreamOptions = {
@@ -344,19 +358,14 @@ bucket_definitions:
344
358
 
345
359
  await storage.clear();
346
360
 
347
- // 3. Start initial replication, then streaming, but don't wait for any of this
361
+ // 3. Start replication, but don't wait for it
348
362
  let initialReplicationDone = false;
349
- streamPromise = (async () => {
350
- await walStream.initReplication(replicationConnection);
351
- initialReplicationDone = true;
352
- await walStream.streamChanges(replicationConnection);
353
- })()
354
- .catch((e) => {
363
+ streamPromise = walStream.replicate();
364
+ walStream
365
+ .waitForInitialSnapshot()
366
+ .catch((_) => {})
367
+ .finally(() => {
355
368
  initialReplicationDone = true;
356
- throw e;
357
- })
358
- .then((v) => {
359
- return v;
360
369
  });
361
370
 
362
371
  // 4. While initial replication is still running, write more changes
@@ -399,8 +408,104 @@ bucket_definitions:
399
408
  }
400
409
 
401
410
  abortController.abort();
402
- await streamPromise;
411
+ await streamPromise.catch((e) => {
412
+ if (e instanceof ReplicationAbortedError) {
413
+ // Ignore
414
+ } else {
415
+ throw e;
416
+ }
417
+ });
403
418
  await connections.end();
404
419
  }
405
420
  });
421
+
422
+ // Test repeatedly performing initial replication while deleting data.
423
+ //
424
+ // This specifically checks for data in the initial snapshot being deleted while snapshotting.
425
+ test('repeated initial replication with deletes', { timeout: TEST_DURATION_MS + TIMEOUT_MARGIN_MS }, async () => {
426
+ const syncRuleContent = `
427
+ bucket_definitions:
428
+ global:
429
+ data:
430
+ - SELECT id, description FROM "test_data"
431
+ `;
432
+
433
+ const start = Date.now();
434
+ let i = 0;
435
+
436
+ while (Date.now() - start < TEST_DURATION_MS) {
437
+ i += 1;
438
+
439
+ // 1. Each iteration starts with a clean slate
440
+ await using context = await WalStreamTestContext.open(factory, {
441
+ walStreamOptions: { snapshotChunkLength: 100 }
442
+ });
443
+ const pool = context.pool;
444
+
445
+ // Introduce an artificial delay in snapshot queries, to make it more likely to reproduce an
446
+ // issue.
447
+ const originalSnapshotConnectionFn = context.connectionManager.snapshotConnection;
448
+ context.connectionManager.snapshotConnection = async () => {
449
+ const conn = await originalSnapshotConnectionFn.call(context.connectionManager);
450
+ // Wrap streaming query to add delays to snapshots
451
+ const originalStream = conn.stream;
452
+ conn.stream = async function* (...args: any[]) {
453
+ const delay = Math.random() * 20;
454
+ yield* originalStream.call(this, ...args);
455
+ await new Promise((resolve) => setTimeout(resolve, delay));
456
+ };
457
+ return conn;
458
+ };
459
+
460
+ await pool.query(`CREATE TABLE test_data(id uuid primary key default uuid_generate_v4(), description text)`);
461
+ await context.updateSyncRules(syncRuleContent);
462
+
463
+ let statements: pgwire.Statement[] = [];
464
+
465
+ const n = Math.floor(Math.random() * 200);
466
+ for (let i = 0; i < n; i++) {
467
+ statements.push({
468
+ statement: `INSERT INTO test_data(description) VALUES('test_init') RETURNING id`
469
+ });
470
+ }
471
+ const results = await pool.query(...statements);
472
+ const ids = new Set(
473
+ results.results.map((sub) => {
474
+ return sub.rows[0].decodeWithoutCustomTypes(0) as string;
475
+ })
476
+ );
477
+
478
+ // 3. Start replication, but don't wait for it
479
+ let initialReplicationDone = false;
480
+
481
+ streamPromise = context.replicateSnapshot().finally(() => {
482
+ initialReplicationDone = true;
483
+ });
484
+
485
+ // 4. While initial replication is still running, delete random rows
486
+ while (!initialReplicationDone && ids.size > 0) {
487
+ let statements: pgwire.Statement[] = [];
488
+
489
+ const m = Math.floor(Math.random() * 10) + 1;
490
+ const idArray = Array.from(ids);
491
+ for (let i = 0; i < m; i++) {
492
+ const id = idArray[Math.floor(Math.random() * idArray.length)];
493
+ statements.push({
494
+ statement: `DELETE FROM test_data WHERE id = $1`,
495
+ params: [{ type: 'uuid', value: id }]
496
+ });
497
+ ids.delete(id);
498
+ }
499
+ await pool.query(...statements);
500
+ await new Promise((resolve) => setTimeout(resolve, Math.random() * 10));
501
+ }
502
+
503
+ await streamPromise;
504
+
505
+ // 5. Once initial replication is done, wait for the streaming changes to complete syncing.
506
+ const data = await context.getBucketData('global[]', 0n);
507
+ const normalized = reduceBucket(data).filter((op) => op.op !== 'CLEAR');
508
+ expect(normalized.length).toEqual(ids.size);
509
+ }
510
+ });
406
511
  }
@@ -7,9 +7,9 @@ describe.skipIf(!env.TEST_POSTGRES_STORAGE)('replication storage combination - p
7
7
  test('should allow the same Postgres cluster to be used for data and storage', async () => {
8
8
  // Use the same cluster for the storage as the data source
9
9
  await using context = await WalStreamTestContext.open(
10
- postgres_storage.test_utils.postgresTestStorageFactoryGenerator({
10
+ postgres_storage.test_utils.postgresTestSetup({
11
11
  url: env.PG_TEST_URL
12
- }),
12
+ }).factory,
13
13
  { doNotClear: false }
14
14
  );
15
15
 
package/test/src/util.ts CHANGED
@@ -2,12 +2,20 @@ import { PostgresRouteAPIAdapter } from '@module/api/PostgresRouteAPIAdapter.js'
2
2
  import * as types from '@module/types/types.js';
3
3
  import * as lib_postgres from '@powersync/lib-service-postgres';
4
4
  import { logger } from '@powersync/lib-services-framework';
5
- import { BucketStorageFactory, InternalOpId, TestStorageFactory } from '@powersync/service-core';
5
+ import {
6
+ BucketStorageFactory,
7
+ CURRENT_STORAGE_VERSION,
8
+ InternalOpId,
9
+ LEGACY_STORAGE_VERSION,
10
+ SUPPORTED_STORAGE_VERSIONS,
11
+ TestStorageConfig,
12
+ TestStorageFactory
13
+ } from '@powersync/service-core';
6
14
  import * as pgwire from '@powersync/service-jpgwire';
7
15
  import * as mongo_storage from '@powersync/service-module-mongodb-storage';
8
16
  import * as postgres_storage from '@powersync/service-module-postgres-storage';
9
- import { env } from './env.js';
10
17
  import { describe, TestOptions } from 'vitest';
18
+ import { env } from './env.js';
11
19
 
12
20
  export const TEST_URI = env.PG_TEST_URL;
13
21
 
@@ -16,18 +24,38 @@ export const INITIALIZED_MONGO_STORAGE_FACTORY = mongo_storage.test_utils.mongoT
16
24
  isCI: env.CI
17
25
  });
18
26
 
19
- export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.postgresTestStorageFactoryGenerator({
27
+ export const INITIALIZED_POSTGRES_STORAGE_FACTORY = postgres_storage.test_utils.postgresTestSetup({
20
28
  url: env.PG_STORAGE_TEST_URL
21
29
  });
22
30
 
23
- export function describeWithStorage(options: TestOptions, fn: (factory: TestStorageFactory) => void) {
24
- describe.skipIf(!env.TEST_MONGO_STORAGE)(`mongodb storage`, options, function () {
25
- fn(INITIALIZED_MONGO_STORAGE_FACTORY);
26
- });
31
+ const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
32
+
33
+ export interface StorageVersionTestContext {
34
+ factory: TestStorageFactory;
35
+ storageVersion: number;
36
+ }
27
37
 
28
- describe.skipIf(!env.TEST_POSTGRES_STORAGE)(`postgres storage`, options, function () {
29
- fn(INITIALIZED_POSTGRES_STORAGE_FACTORY);
30
- });
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
+ }
31
59
  }
32
60
 
33
61
  export const TEST_CONNECTION_OPTIONS = types.normalizeConnectionConfig({
@@ -3,9 +3,10 @@ import { expect, test } from 'vitest';
3
3
 
4
4
  import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
5
5
  import { WalStreamTestContext } from './wal_stream_utils.js';
6
+ import { updateSyncRulesFromYaml } from '@powersync/service-core';
6
7
 
7
8
  test('validate tables', async () => {
8
- await using context = await WalStreamTestContext.open(INITIALIZED_MONGO_STORAGE_FACTORY);
9
+ await using context = await WalStreamTestContext.open(INITIALIZED_MONGO_STORAGE_FACTORY.factory);
9
10
  const { pool } = context;
10
11
 
11
12
  await pool.query(`CREATE TABLE test_data(id uuid primary key default uuid_generate_v4(), description text)`);
@@ -19,7 +20,7 @@ bucket_definitions:
19
20
  - SELECT * FROM "other%"
20
21
  `;
21
22
 
22
- const syncRules = await context.factory.updateSyncRules({ content: syncRuleContent });
23
+ const syncRules = await context.factory.updateSyncRules(updateSyncRulesFromYaml(syncRuleContent));
23
24
 
24
25
  const tablePatterns = syncRules.parsed({ defaultSchema: 'public' }).sync_rules.config.getSourceTables();
25
26
  const tableInfo = await getDebugTablesInfo({
@@ -1,13 +1,12 @@
1
1
  import { MissingReplicationSlotError } from '@module/replication/WalStream.js';
2
- import { storage } from '@powersync/service-core';
3
2
  import { METRICS_HELPER, putOp, removeOp } from '@powersync/service-core-tests';
4
3
  import { pgwireRows } from '@powersync/service-jpgwire';
4
+ import { JSONBig } from '@powersync/service-jsonbig';
5
5
  import { ReplicationMetric } from '@powersync/service-types';
6
6
  import * as crypto from 'crypto';
7
- import { afterAll, beforeAll, describe, expect, test } from 'vitest';
8
- import { describeWithStorage } from './util.js';
7
+ import { describe, expect, test } from 'vitest';
8
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
9
9
  import { WalStreamTestContext, withMaxWalSize } from './wal_stream_utils.js';
10
- import { JSONBig } from '@powersync/service-jsonbig';
11
10
 
12
11
  const BASIC_SYNC_RULES = `
13
12
  bucket_definitions:
@@ -20,9 +19,12 @@ describe('wal stream', () => {
20
19
  describeWithStorage({ timeout: 20_000 }, defineWalStreamTests);
21
20
  });
22
21
 
23
- function defineWalStreamTests(factory: storage.TestStorageFactory) {
22
+ function defineWalStreamTests({ factory, storageVersion }: StorageVersionTestContext) {
23
+ const openContext = (options?: Parameters<typeof WalStreamTestContext.open>[1]) => {
24
+ return WalStreamTestContext.open(factory, { ...options, storageVersion });
25
+ };
24
26
  test('replicating basic values', async () => {
25
- await using context = await WalStreamTestContext.open(factory);
27
+ await using context = await openContext();
26
28
  const { pool } = context;
27
29
  await context.updateSyncRules(`
28
30
  bucket_definitions:
@@ -57,7 +59,7 @@ bucket_definitions:
57
59
  });
58
60
 
59
61
  test('replicating case sensitive table', async () => {
60
- await using context = await WalStreamTestContext.open(factory);
62
+ await using context = await openContext();
61
63
  const { pool } = context;
62
64
  await context.updateSyncRules(`
63
65
  bucket_definitions:
@@ -88,7 +90,7 @@ bucket_definitions:
88
90
  });
89
91
 
90
92
  test('replicating TOAST values', async () => {
91
- await using context = await WalStreamTestContext.open(factory);
93
+ await using context = await openContext();
92
94
  const { pool } = context;
93
95
  await context.updateSyncRules(`
94
96
  bucket_definitions:
@@ -103,7 +105,6 @@ bucket_definitions:
103
105
  );
104
106
 
105
107
  await context.replicateSnapshot();
106
- context.startStreaming();
107
108
 
108
109
  // Must be > 8kb after compression
109
110
  const largeDescription = crypto.randomBytes(20_000).toString('hex');
@@ -126,7 +127,7 @@ bucket_definitions:
126
127
  });
127
128
 
128
129
  test('replicating TRUNCATE', async () => {
129
- await using context = await WalStreamTestContext.open(factory);
130
+ await using context = await openContext();
130
131
  const { pool } = context;
131
132
  const syncRuleContent = `
132
133
  bucket_definitions:
@@ -157,7 +158,7 @@ bucket_definitions:
157
158
  });
158
159
 
159
160
  test('replicating changing primary key', async () => {
160
- await using context = await WalStreamTestContext.open(factory);
161
+ await using context = await openContext();
161
162
  const { pool } = context;
162
163
  await context.updateSyncRules(BASIC_SYNC_RULES);
163
164
  await pool.query(`DROP TABLE IF EXISTS test_data`);
@@ -198,7 +199,7 @@ bucket_definitions:
198
199
  });
199
200
 
200
201
  test('initial sync', async () => {
201
- await using context = await WalStreamTestContext.open(factory);
202
+ await using context = await openContext();
202
203
  const { pool } = context;
203
204
  await context.updateSyncRules(BASIC_SYNC_RULES);
204
205
 
@@ -210,14 +211,13 @@ bucket_definitions:
210
211
  );
211
212
 
212
213
  await context.replicateSnapshot();
213
- context.startStreaming();
214
214
 
215
215
  const data = await context.getBucketData('global[]');
216
216
  expect(data).toMatchObject([putOp('test_data', { id: test_id, description: 'test1' })]);
217
217
  });
218
218
 
219
219
  test('record too large', async () => {
220
- await using context = await WalStreamTestContext.open(factory);
220
+ await using context = await openContext();
221
221
  await context.updateSyncRules(`bucket_definitions:
222
222
  global:
223
223
  data:
@@ -242,8 +242,6 @@ bucket_definitions:
242
242
  params: [{ type: 'varchar', value: largeDescription }]
243
243
  });
244
244
 
245
- context.startStreaming();
246
-
247
245
  const data = await context.getBucketData('global[]');
248
246
  expect(data.length).toEqual(1);
249
247
  const row = JSON.parse(data[0].data as string);
@@ -254,7 +252,7 @@ bucket_definitions:
254
252
  });
255
253
 
256
254
  test('table not in sync rules', async () => {
257
- await using context = await WalStreamTestContext.open(factory);
255
+ await using context = await openContext();
258
256
  const { pool } = context;
259
257
  await context.updateSyncRules(BASIC_SYNC_RULES);
260
258
 
@@ -280,7 +278,7 @@ bucket_definitions:
280
278
 
281
279
  test('reporting slot issues', async () => {
282
280
  {
283
- await using context = await WalStreamTestContext.open(factory);
281
+ await using context = await openContext();
284
282
  const { pool } = context;
285
283
  await context.updateSyncRules(`
286
284
  bucket_definitions:
@@ -295,7 +293,6 @@ bucket_definitions:
295
293
  `INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
296
294
  );
297
295
  await context.replicateSnapshot();
298
- context.startStreaming();
299
296
 
300
297
  const data = await context.getBucketData('global[]');
301
298
 
@@ -310,7 +307,7 @@ bucket_definitions:
310
307
  }
311
308
 
312
309
  {
313
- await using context = await WalStreamTestContext.open(factory, { doNotClear: true });
310
+ await using context = await openContext({ doNotClear: true });
314
311
  const { pool } = context;
315
312
  await pool.query('DROP PUBLICATION powersync');
316
313
  await pool.query(`UPDATE test_data SET description = 'updated'`);
@@ -320,15 +317,12 @@ bucket_definitions:
320
317
 
321
318
  await context.loadActiveSyncRules();
322
319
 
323
- // Previously, the `replicateSnapshot` call picked up on this error.
324
- // Now, we have removed that check, this only comes up when we start actually streaming.
325
- // We don't get the streaming response directly here, but getCheckpoint() checks for that.
326
- await context.replicateSnapshot();
327
- context.startStreaming();
320
+ // Note: The actual error may be thrown either in replicateSnapshot(), or in getCheckpoint().
328
321
 
329
322
  if (serverVersion!.compareMain('18.0.0') >= 0) {
330
323
  // No error expected in Postres 18. Replication keeps on working depite the
331
324
  // publication being re-created.
325
+ await context.replicateSnapshot();
332
326
  await context.getCheckpoint();
333
327
  } else {
334
328
  // await context.getCheckpoint();
@@ -336,16 +330,16 @@ bucket_definitions:
336
330
  // In the service, this error is handled in WalStreamReplicationJob,
337
331
  // creating a new replication slot.
338
332
  await expect(async () => {
333
+ await context.replicateSnapshot();
339
334
  await context.getCheckpoint();
340
335
  }).rejects.toThrowError(MissingReplicationSlotError);
341
- context.clearStreamError();
342
336
  }
343
337
  }
344
338
  });
345
339
 
346
340
  test('dropped replication slot', async () => {
347
341
  {
348
- await using context = await WalStreamTestContext.open(factory);
342
+ await using context = await openContext();
349
343
  const { pool } = context;
350
344
  await context.updateSyncRules(`
351
345
  bucket_definitions:
@@ -360,7 +354,6 @@ bucket_definitions:
360
354
  `INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
361
355
  );
362
356
  await context.replicateSnapshot();
363
- context.startStreaming();
364
357
 
365
358
  const data = await context.getBucketData('global[]');
366
359
 
@@ -375,7 +368,7 @@ bucket_definitions:
375
368
  }
376
369
 
377
370
  {
378
- await using context = await WalStreamTestContext.open(factory, { doNotClear: true });
371
+ await using context = await openContext({ doNotClear: true });
379
372
  const { pool } = context;
380
373
  const storage = await context.factory.getActiveStorage();
381
374
 
@@ -396,7 +389,7 @@ bucket_definitions:
396
389
  });
397
390
 
398
391
  test('replication slot lost', async () => {
399
- await using baseContext = await WalStreamTestContext.open(factory, { doNotClear: true });
392
+ await using baseContext = await openContext({ doNotClear: true });
400
393
 
401
394
  const serverVersion = await baseContext.connectionManager.getServerVersion();
402
395
  if (serverVersion!.compareMain('13.0.0') < 0) {
@@ -408,7 +401,7 @@ bucket_definitions:
408
401
  await using s = await withMaxWalSize(baseContext.pool, '100MB');
409
402
 
410
403
  {
411
- await using context = await WalStreamTestContext.open(factory);
404
+ await using context = await openContext();
412
405
  const { pool } = context;
413
406
  await context.updateSyncRules(`
414
407
  bucket_definitions:
@@ -423,7 +416,6 @@ bucket_definitions:
423
416
  `INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
424
417
  );
425
418
  await context.replicateSnapshot();
426
- context.startStreaming();
427
419
 
428
420
  const data = await context.getBucketData('global[]');
429
421
 
@@ -438,7 +430,7 @@ bucket_definitions:
438
430
  }
439
431
 
440
432
  {
441
- await using context = await WalStreamTestContext.open(factory, { doNotClear: true });
433
+ await using context = await openContext({ doNotClear: true });
442
434
  const { pool } = context;
443
435
  const storage = await context.factory.getActiveStorage();
444
436
  const slotName = storage?.slot_name!;
@@ -479,7 +471,7 @@ bucket_definitions:
479
471
  });
480
472
 
481
473
  test('old date format', async () => {
482
- await using context = await WalStreamTestContext.open(factory);
474
+ await using context = await openContext();
483
475
  await context.updateSyncRules(BASIC_SYNC_RULES);
484
476
 
485
477
  const { pool } = context;
@@ -494,7 +486,7 @@ bucket_definitions:
494
486
  });
495
487
 
496
488
  test('new date format', async () => {
497
- await using context = await WalStreamTestContext.open(factory);
489
+ await using context = await openContext();
498
490
  await context.updateSyncRules(`
499
491
  streams:
500
492
  stream:
@@ -510,12 +502,12 @@ config:
510
502
  await context.initializeReplication();
511
503
  await pool.query(`INSERT INTO test_data(id, description) VALUES ('t1', '2025-09-10 15:17:14+02')`);
512
504
 
513
- const data = await context.getBucketData('1#stream|0[]');
505
+ const data = await context.getBucketData('stream|0[]');
514
506
  expect(data).toMatchObject([putOp('test_data', { id: 't1', description: '2025-09-10T13:17:14.000000Z' })]);
515
507
  });
516
508
 
517
509
  test('custom types', async () => {
518
- await using context = await WalStreamTestContext.open(factory);
510
+ await using context = await openContext();
519
511
 
520
512
  await context.updateSyncRules(`
521
513
  streams:
@@ -542,7 +534,7 @@ config:
542
534
  `INSERT INTO test_data(id, description, ts) VALUES ('t2', ROW(TRUE, 2)::composite, '2025-11-17T09:12:00Z')`
543
535
  );
544
536
 
545
- const data = await context.getBucketData('1#stream|0[]');
537
+ const data = await context.getBucketData('stream|0[]');
546
538
  expect(data).toMatchObject([
547
539
  putOp('test_data', { id: 't1', description: '{"foo":1,"bar":1}', ts: '2025-11-17T09:11:00.000000Z' }),
548
540
  putOp('test_data', { id: 't2', description: '{"foo":1,"bar":2}', ts: '2025-11-17T09:12:00.000000Z' })
@@ -550,7 +542,7 @@ config:
550
542
  });
551
543
 
552
544
  test('custom types in primary key', async () => {
553
- await using context = await WalStreamTestContext.open(factory);
545
+ await using context = await openContext();
554
546
 
555
547
  await context.updateSyncRules(`
556
548
  streams:
@@ -569,14 +561,14 @@ config:
569
561
  await context.initializeReplication();
570
562
  await pool.query(`INSERT INTO test_data(id) VALUES ('t1')`);
571
563
 
572
- const data = await context.getBucketData('1#stream|0[]');
564
+ const data = await context.getBucketData('stream|0[]');
573
565
  expect(data).toMatchObject([putOp('test_data', { id: 't1' })]);
574
566
  });
575
567
 
576
568
  test('replica identity handling', async () => {
577
569
  // This specifically test a case of timestamps being used as part of the replica identity.
578
570
  // There was a regression in versions 1.15.0-1.15.5, which this tests for.
579
- await using context = await WalStreamTestContext.open(factory);
571
+ await using context = await openContext();
580
572
  const { pool } = context;
581
573
  await context.updateSyncRules(BASIC_SYNC_RULES);
582
574
 
@@ -591,7 +583,6 @@ config:
591
583
  );
592
584
 
593
585
  await context.replicateSnapshot();
594
- context.startStreaming();
595
586
 
596
587
  await pool.query(`UPDATE test_data SET description = 'test2' WHERE id = '${test_id}'`);
597
588