@powersync/service-module-postgres 0.16.10 → 0.16.12
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 +25 -0
- package/dist/api/PostgresRouteAPIAdapter.js +1 -2
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.d.ts +0 -1
- package/dist/module/PostgresModule.js +4 -9
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/ConnectionManagerFactory.d.ts +3 -5
- package/dist/replication/ConnectionManagerFactory.js +11 -9
- package/dist/replication/ConnectionManagerFactory.js.map +1 -1
- package/dist/replication/PgManager.d.ts +6 -4
- package/dist/replication/PgManager.js +17 -7
- package/dist/replication/PgManager.js.map +1 -1
- package/dist/replication/PostgresErrorRateLimiter.js +6 -1
- package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
- package/dist/replication/WalStream.d.ts +2 -5
- package/dist/replication/WalStream.js +69 -96
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicationJob.d.ts +1 -2
- package/dist/replication/WalStreamReplicationJob.js +47 -70
- package/dist/replication/WalStreamReplicationJob.js.map +1 -1
- package/dist/types/resolver.d.ts +9 -3
- package/dist/types/resolver.js +26 -5
- package/dist/types/resolver.js.map +1 -1
- package/dist/types/types.d.ts +1 -4
- package/dist/types/types.js.map +1 -1
- package/package.json +10 -10
- package/src/api/PostgresRouteAPIAdapter.ts +1 -1
- package/src/module/PostgresModule.ts +4 -9
- package/src/replication/ConnectionManagerFactory.ts +14 -13
- package/src/replication/PgManager.ts +22 -11
- package/src/replication/PostgresErrorRateLimiter.ts +5 -1
- package/src/replication/WalStream.ts +76 -117
- package/src/replication/WalStreamReplicationJob.ts +48 -68
- package/src/types/resolver.ts +27 -6
- package/src/types/types.ts +1 -5
- package/test/src/pg_test.test.ts +2 -2
- package/test/src/slow_tests.test.ts +2 -2
- package/test/src/wal_stream.test.ts +29 -10
- package/test/src/wal_stream_utils.ts +24 -5
- package/tsconfig.tsbuildinfo +1 -1
package/test/src/pg_test.test.ts
CHANGED
|
@@ -551,7 +551,7 @@ INSERT INTO test_data(id, time, timestamp, timestamptz) VALUES (1, '17:42:01.12'
|
|
|
551
551
|
test('test replication - multiranges', async () => {
|
|
552
552
|
const db = await connectPgPool();
|
|
553
553
|
|
|
554
|
-
if (!(await new PostgresTypeResolver(
|
|
554
|
+
if (!(await new PostgresTypeResolver(db).supportsMultiRanges())) {
|
|
555
555
|
// This test requires Postgres 14 or later.
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
@@ -620,7 +620,7 @@ INSERT INTO test_data(id, time, timestamp, timestamptz) VALUES (1, '17:42:01.12'
|
|
|
620
620
|
* Return all the inserts from the first transaction in the replication stream.
|
|
621
621
|
*/
|
|
622
622
|
async function getReplicationTx(db: pgwire.PgClient, replicationStream: pgwire.ReplicationStream) {
|
|
623
|
-
const typeCache = new PostgresTypeResolver(
|
|
623
|
+
const typeCache = new PostgresTypeResolver(db);
|
|
624
624
|
await typeCache.fetchTypesForSchema();
|
|
625
625
|
|
|
626
626
|
let transformed: SqliteInputRow[] = [];
|
|
@@ -69,7 +69,7 @@ function defineSlowTests(factory: storage.TestStorageFactory) {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
async function testRepeatedReplication(testOptions: { compact: boolean; maxBatchSize: number; numBatches: number }) {
|
|
72
|
-
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
72
|
+
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
73
73
|
const replicationConnection = await connections.replicationConnection();
|
|
74
74
|
const pool = connections.pool;
|
|
75
75
|
await clearTestDb(pool);
|
|
@@ -330,7 +330,7 @@ bucket_definitions:
|
|
|
330
330
|
await pool.query(`SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE active = FALSE`);
|
|
331
331
|
i += 1;
|
|
332
332
|
|
|
333
|
-
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
333
|
+
const connections = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
334
334
|
const replicationConnection = await connections.replicationConnection();
|
|
335
335
|
|
|
336
336
|
abortController = new AbortController();
|
|
@@ -295,7 +295,7 @@ bucket_definitions:
|
|
|
295
295
|
`INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
|
|
296
296
|
);
|
|
297
297
|
await context.replicateSnapshot();
|
|
298
|
-
|
|
298
|
+
context.startStreaming();
|
|
299
299
|
|
|
300
300
|
const data = await context.getBucketData('global[]');
|
|
301
301
|
|
|
@@ -320,17 +320,25 @@ bucket_definitions:
|
|
|
320
320
|
|
|
321
321
|
await context.loadActiveSyncRules();
|
|
322
322
|
|
|
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();
|
|
328
|
+
|
|
323
329
|
if (serverVersion!.compareMain('18.0.0') >= 0) {
|
|
324
|
-
await context.replicateSnapshot();
|
|
325
330
|
// No error expected in Postres 18. Replication keeps on working depite the
|
|
326
331
|
// publication being re-created.
|
|
332
|
+
await context.getCheckpoint();
|
|
327
333
|
} else {
|
|
334
|
+
// await context.getCheckpoint();
|
|
328
335
|
// Postgres < 18 invalidates the replication slot when the publication is re-created.
|
|
329
|
-
//
|
|
336
|
+
// In the service, this error is handled in WalStreamReplicationJob,
|
|
330
337
|
// creating a new replication slot.
|
|
331
338
|
await expect(async () => {
|
|
332
|
-
await context.
|
|
339
|
+
await context.getCheckpoint();
|
|
333
340
|
}).rejects.toThrowError(MissingReplicationSlotError);
|
|
341
|
+
context.clearStreamError();
|
|
334
342
|
}
|
|
335
343
|
}
|
|
336
344
|
});
|
|
@@ -352,7 +360,7 @@ bucket_definitions:
|
|
|
352
360
|
`INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
|
|
353
361
|
);
|
|
354
362
|
await context.replicateSnapshot();
|
|
355
|
-
|
|
363
|
+
context.startStreaming();
|
|
356
364
|
|
|
357
365
|
const data = await context.getBucketData('global[]');
|
|
358
366
|
|
|
@@ -415,7 +423,7 @@ bucket_definitions:
|
|
|
415
423
|
`INSERT INTO test_data(id, description) VALUES('8133cd37-903b-4937-a022-7c8294015a3a', 'test1') returning id as test_id`
|
|
416
424
|
);
|
|
417
425
|
await context.replicateSnapshot();
|
|
418
|
-
|
|
426
|
+
context.startStreaming();
|
|
419
427
|
|
|
420
428
|
const data = await context.getBucketData('global[]');
|
|
421
429
|
|
|
@@ -521,13 +529,24 @@ config:
|
|
|
521
529
|
const { pool } = context;
|
|
522
530
|
await pool.query(`DROP TABLE IF EXISTS test_data`);
|
|
523
531
|
await pool.query(`CREATE TYPE composite AS (foo bool, bar int4);`);
|
|
524
|
-
await pool.query(`CREATE TABLE test_data(id text primary key, description composite);`);
|
|
532
|
+
await pool.query(`CREATE TABLE test_data(id text primary key, description composite, ts timestamptz);`);
|
|
533
|
+
|
|
534
|
+
// Covered by initial replication
|
|
535
|
+
await pool.query(
|
|
536
|
+
`INSERT INTO test_data(id, description, ts) VALUES ('t1', ROW(TRUE, 1)::composite, '2025-11-17T09:11:00Z')`
|
|
537
|
+
);
|
|
525
538
|
|
|
526
539
|
await context.initializeReplication();
|
|
527
|
-
|
|
540
|
+
// Covered by streaming replication
|
|
541
|
+
await pool.query(
|
|
542
|
+
`INSERT INTO test_data(id, description, ts) VALUES ('t2', ROW(TRUE, 2)::composite, '2025-11-17T09:12:00Z')`
|
|
543
|
+
);
|
|
528
544
|
|
|
529
545
|
const data = await context.getBucketData('1#stream|0[]');
|
|
530
|
-
expect(data).toMatchObject([
|
|
546
|
+
expect(data).toMatchObject([
|
|
547
|
+
putOp('test_data', { id: 't1', description: '{"foo":1,"bar":1}', ts: '2025-11-17T09:11:00.000000Z' }),
|
|
548
|
+
putOp('test_data', { id: 't2', description: '{"foo":1,"bar":2}', ts: '2025-11-17T09:12:00.000000Z' })
|
|
549
|
+
]);
|
|
531
550
|
});
|
|
532
551
|
|
|
533
552
|
test('custom types in primary key', async () => {
|
|
@@ -572,7 +591,7 @@ config:
|
|
|
572
591
|
);
|
|
573
592
|
|
|
574
593
|
await context.replicateSnapshot();
|
|
575
|
-
|
|
594
|
+
context.startStreaming();
|
|
576
595
|
|
|
577
596
|
await pool.query(`UPDATE test_data SET description = 'test2' WHERE id = '${test_id}'`);
|
|
578
597
|
|
|
@@ -33,7 +33,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
33
33
|
options?: { doNotClear?: boolean; walStreamOptions?: Partial<WalStreamOptions> }
|
|
34
34
|
) {
|
|
35
35
|
const f = await factory({ doNotClear: options?.doNotClear });
|
|
36
|
-
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {
|
|
36
|
+
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
37
37
|
|
|
38
38
|
if (!options?.doNotClear) {
|
|
39
39
|
await clearTestDb(connectionManager.pool);
|
|
@@ -55,12 +55,31 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
55
55
|
await this.dispose();
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Clear any errors from startStream, to allow for a graceful dispose when streaming errors
|
|
60
|
+
* were expected.
|
|
61
|
+
*/
|
|
62
|
+
async clearStreamError() {
|
|
63
|
+
if (this.streamPromise != null) {
|
|
64
|
+
this.streamPromise = this.streamPromise.catch((e) => {});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
async dispose() {
|
|
59
69
|
this.abortController.abort();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
try {
|
|
71
|
+
await this.snapshotPromise;
|
|
72
|
+
await this.streamPromise;
|
|
73
|
+
await this.connectionManager.destroy();
|
|
74
|
+
await this.factory?.[Symbol.asyncDispose]();
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Throwing here may result in SuppressedError. The underlying errors often don't show up
|
|
77
|
+
// in the test output, so we log it here.
|
|
78
|
+
// If we could get vitest to log SuppressedError.error and SuppressedError.suppressed, we
|
|
79
|
+
// could remove this.
|
|
80
|
+
console.error('Error during WalStreamTestContext dispose', e);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
get pool() {
|