@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.
- package/CHANGELOG.md +49 -0
- package/dist/replication/WalStream.d.ts +9 -2
- package/dist/replication/WalStream.js +29 -10
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/replication-utils.js +1 -1
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/types/types.d.ts +3 -0
- package/package.json +11 -11
- package/src/replication/WalStream.ts +36 -17
- package/src/replication/replication-utils.ts +1 -1
- package/test/src/checkpoints.test.ts +6 -8
- package/test/src/chunked_snapshots.test.ts +10 -5
- package/test/src/large_batch.test.ts +16 -28
- package/test/src/pg_test.test.ts +5 -5
- package/test/src/resuming_snapshots.test.ts +24 -20
- package/test/src/route_api_adapter.test.ts +5 -3
- package/test/src/schema_changes.test.ts +74 -92
- package/test/src/slow_tests.test.ts +134 -29
- package/test/src/storage_combination.test.ts +2 -2
- package/test/src/util.ts +38 -10
- package/test/src/validation.test.ts +3 -2
- package/test/src/wal_stream.test.ts +33 -42
- package/test/src/wal_stream_utils.ts +80 -42
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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 {
|
|
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 {
|
|
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:
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
361
|
+
// 3. Start replication, but don't wait for it
|
|
348
362
|
let initialReplicationDone = false;
|
|
349
|
-
streamPromise = (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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.
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
|
|
32
|
+
|
|
33
|
+
export interface StorageVersionTestContext {
|
|
34
|
+
factory: TestStorageFactory;
|
|
35
|
+
storageVersion: number;
|
|
36
|
+
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
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(
|
|
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 {
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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('
|
|
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
|
|
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
|
|