@powersync/service-module-postgres 0.17.1 → 0.18.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 +39 -0
- package/dist/types/types.d.ts +3 -0
- package/package.json +11 -11
- package/test/src/checkpoints.test.ts +4 -4
- package/test/src/chunked_snapshots.test.ts +8 -4
- package/test/src/large_batch.test.ts +16 -22
- package/test/src/resuming_snapshots.test.ts +14 -7
- package/test/src/route_api_adapter.test.ts +3 -1
- package/test/src/schema_changes.test.ts +20 -18
- package/test/src/slow_tests.test.ts +12 -6
- package/test/src/util.ts +35 -9
- package/test/src/validation.test.ts +2 -1
- package/test/src/wal_stream.test.ts +26 -24
- package/test/src/wal_stream_utils.ts +56 -10
- package/test/tsconfig.json +3 -7
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,10 @@
|
|
|
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';
|
|
5
4
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
6
5
|
import * as crypto from 'crypto';
|
|
7
|
-
import {
|
|
8
|
-
import { describeWithStorage } from './util.js';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
7
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
9
8
|
import { WalStreamTestContext, withMaxWalSize } from './wal_stream_utils.js';
|
|
10
9
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
11
10
|
|
|
@@ -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:
|
|
@@ -126,7 +128,7 @@ bucket_definitions:
|
|
|
126
128
|
});
|
|
127
129
|
|
|
128
130
|
test('replicating TRUNCATE', async () => {
|
|
129
|
-
await using context = await
|
|
131
|
+
await using context = await openContext();
|
|
130
132
|
const { pool } = context;
|
|
131
133
|
const syncRuleContent = `
|
|
132
134
|
bucket_definitions:
|
|
@@ -157,7 +159,7 @@ bucket_definitions:
|
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
test('replicating changing primary key', async () => {
|
|
160
|
-
await using context = await
|
|
162
|
+
await using context = await openContext();
|
|
161
163
|
const { pool } = context;
|
|
162
164
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
163
165
|
await pool.query(`DROP TABLE IF EXISTS test_data`);
|
|
@@ -198,7 +200,7 @@ bucket_definitions:
|
|
|
198
200
|
});
|
|
199
201
|
|
|
200
202
|
test('initial sync', async () => {
|
|
201
|
-
await using context = await
|
|
203
|
+
await using context = await openContext();
|
|
202
204
|
const { pool } = context;
|
|
203
205
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
204
206
|
|
|
@@ -217,7 +219,7 @@ bucket_definitions:
|
|
|
217
219
|
});
|
|
218
220
|
|
|
219
221
|
test('record too large', async () => {
|
|
220
|
-
await using context = await
|
|
222
|
+
await using context = await openContext();
|
|
221
223
|
await context.updateSyncRules(`bucket_definitions:
|
|
222
224
|
global:
|
|
223
225
|
data:
|
|
@@ -254,7 +256,7 @@ bucket_definitions:
|
|
|
254
256
|
});
|
|
255
257
|
|
|
256
258
|
test('table not in sync rules', async () => {
|
|
257
|
-
await using context = await
|
|
259
|
+
await using context = await openContext();
|
|
258
260
|
const { pool } = context;
|
|
259
261
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
260
262
|
|
|
@@ -280,7 +282,7 @@ bucket_definitions:
|
|
|
280
282
|
|
|
281
283
|
test('reporting slot issues', async () => {
|
|
282
284
|
{
|
|
283
|
-
await using context = await
|
|
285
|
+
await using context = await openContext();
|
|
284
286
|
const { pool } = context;
|
|
285
287
|
await context.updateSyncRules(`
|
|
286
288
|
bucket_definitions:
|
|
@@ -310,7 +312,7 @@ bucket_definitions:
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
{
|
|
313
|
-
await using context = await
|
|
315
|
+
await using context = await openContext({ doNotClear: true });
|
|
314
316
|
const { pool } = context;
|
|
315
317
|
await pool.query('DROP PUBLICATION powersync');
|
|
316
318
|
await pool.query(`UPDATE test_data SET description = 'updated'`);
|
|
@@ -345,7 +347,7 @@ bucket_definitions:
|
|
|
345
347
|
|
|
346
348
|
test('dropped replication slot', async () => {
|
|
347
349
|
{
|
|
348
|
-
await using context = await
|
|
350
|
+
await using context = await openContext();
|
|
349
351
|
const { pool } = context;
|
|
350
352
|
await context.updateSyncRules(`
|
|
351
353
|
bucket_definitions:
|
|
@@ -375,7 +377,7 @@ bucket_definitions:
|
|
|
375
377
|
}
|
|
376
378
|
|
|
377
379
|
{
|
|
378
|
-
await using context = await
|
|
380
|
+
await using context = await openContext({ doNotClear: true });
|
|
379
381
|
const { pool } = context;
|
|
380
382
|
const storage = await context.factory.getActiveStorage();
|
|
381
383
|
|
|
@@ -396,7 +398,7 @@ bucket_definitions:
|
|
|
396
398
|
});
|
|
397
399
|
|
|
398
400
|
test('replication slot lost', async () => {
|
|
399
|
-
await using baseContext = await
|
|
401
|
+
await using baseContext = await openContext({ doNotClear: true });
|
|
400
402
|
|
|
401
403
|
const serverVersion = await baseContext.connectionManager.getServerVersion();
|
|
402
404
|
if (serverVersion!.compareMain('13.0.0') < 0) {
|
|
@@ -408,7 +410,7 @@ bucket_definitions:
|
|
|
408
410
|
await using s = await withMaxWalSize(baseContext.pool, '100MB');
|
|
409
411
|
|
|
410
412
|
{
|
|
411
|
-
await using context = await
|
|
413
|
+
await using context = await openContext();
|
|
412
414
|
const { pool } = context;
|
|
413
415
|
await context.updateSyncRules(`
|
|
414
416
|
bucket_definitions:
|
|
@@ -438,7 +440,7 @@ bucket_definitions:
|
|
|
438
440
|
}
|
|
439
441
|
|
|
440
442
|
{
|
|
441
|
-
await using context = await
|
|
443
|
+
await using context = await openContext({ doNotClear: true });
|
|
442
444
|
const { pool } = context;
|
|
443
445
|
const storage = await context.factory.getActiveStorage();
|
|
444
446
|
const slotName = storage?.slot_name!;
|
|
@@ -479,7 +481,7 @@ bucket_definitions:
|
|
|
479
481
|
});
|
|
480
482
|
|
|
481
483
|
test('old date format', async () => {
|
|
482
|
-
await using context = await
|
|
484
|
+
await using context = await openContext();
|
|
483
485
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
484
486
|
|
|
485
487
|
const { pool } = context;
|
|
@@ -494,7 +496,7 @@ bucket_definitions:
|
|
|
494
496
|
});
|
|
495
497
|
|
|
496
498
|
test('new date format', async () => {
|
|
497
|
-
await using context = await
|
|
499
|
+
await using context = await openContext();
|
|
498
500
|
await context.updateSyncRules(`
|
|
499
501
|
streams:
|
|
500
502
|
stream:
|
|
@@ -515,7 +517,7 @@ config:
|
|
|
515
517
|
});
|
|
516
518
|
|
|
517
519
|
test('custom types', async () => {
|
|
518
|
-
await using context = await
|
|
520
|
+
await using context = await openContext();
|
|
519
521
|
|
|
520
522
|
await context.updateSyncRules(`
|
|
521
523
|
streams:
|
|
@@ -550,7 +552,7 @@ config:
|
|
|
550
552
|
});
|
|
551
553
|
|
|
552
554
|
test('custom types in primary key', async () => {
|
|
553
|
-
await using context = await
|
|
555
|
+
await using context = await openContext();
|
|
554
556
|
|
|
555
557
|
await context.updateSyncRules(`
|
|
556
558
|
streams:
|
|
@@ -576,7 +578,7 @@ config:
|
|
|
576
578
|
test('replica identity handling', async () => {
|
|
577
579
|
// This specifically test a case of timestamps being used as part of the replica identity.
|
|
578
580
|
// There was a regression in versions 1.15.0-1.15.5, which this tests for.
|
|
579
|
-
await using context = await
|
|
581
|
+
await using context = await openContext();
|
|
580
582
|
const { pool } = context;
|
|
581
583
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
582
584
|
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { PgManager } from '@module/replication/PgManager.js';
|
|
2
2
|
import { PUBLICATION_NAME, WalStream, WalStreamOptions } from '@module/replication/WalStream.js';
|
|
3
|
+
import { CustomTypeRegistry } from '@module/types/registry.js';
|
|
3
4
|
import {
|
|
4
5
|
BucketStorageFactory,
|
|
5
6
|
createCoreReplicationMetrics,
|
|
6
7
|
initializeCoreReplicationMetrics,
|
|
7
8
|
InternalOpId,
|
|
9
|
+
LEGACY_STORAGE_VERSION,
|
|
8
10
|
OplogEntry,
|
|
11
|
+
STORAGE_VERSION_CONFIG,
|
|
9
12
|
storage,
|
|
10
|
-
SyncRulesBucketStorage
|
|
13
|
+
SyncRulesBucketStorage,
|
|
14
|
+
updateSyncRulesFromYaml
|
|
11
15
|
} from '@powersync/service-core';
|
|
12
16
|
import { METRICS_HELPER, test_utils } from '@powersync/service-core-tests';
|
|
13
17
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
14
18
|
import { clearTestDb, getClientCheckpoint, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
15
|
-
import { CustomTypeRegistry } from '@module/types/registry.js';
|
|
16
19
|
|
|
17
20
|
export class WalStreamTestContext implements AsyncDisposable {
|
|
18
21
|
private _walStream?: WalStream;
|
|
19
22
|
private abortController = new AbortController();
|
|
20
23
|
private streamPromise?: Promise<void>;
|
|
24
|
+
private syncRulesId?: number;
|
|
21
25
|
public storage?: SyncRulesBucketStorage;
|
|
22
26
|
private replicationConnection?: pgwire.PgConnection;
|
|
23
27
|
private snapshotPromise?: Promise<void>;
|
|
@@ -30,7 +34,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
30
34
|
*/
|
|
31
35
|
static async open(
|
|
32
36
|
factory: (options: storage.TestStorageOptions) => Promise<BucketStorageFactory>,
|
|
33
|
-
options?: { doNotClear?: boolean; walStreamOptions?: Partial<WalStreamOptions> }
|
|
37
|
+
options?: { doNotClear?: boolean; storageVersion?: number; walStreamOptions?: Partial<WalStreamOptions> }
|
|
34
38
|
) {
|
|
35
39
|
const f = await factory({ doNotClear: options?.doNotClear });
|
|
36
40
|
const connectionManager = new PgManager(TEST_CONNECTION_OPTIONS, {});
|
|
@@ -39,13 +43,18 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
39
43
|
await clearTestDb(connectionManager.pool);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
const storageVersion = options?.storageVersion ?? LEGACY_STORAGE_VERSION;
|
|
47
|
+
const versionedBuckets = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false;
|
|
48
|
+
|
|
49
|
+
return new WalStreamTestContext(f, connectionManager, options?.walStreamOptions, storageVersion, versionedBuckets);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
constructor(
|
|
46
53
|
public factory: BucketStorageFactory,
|
|
47
54
|
public connectionManager: PgManager,
|
|
48
|
-
private walStreamOptions?: Partial<WalStreamOptions
|
|
55
|
+
private walStreamOptions?: Partial<WalStreamOptions>,
|
|
56
|
+
private storageVersion: number = LEGACY_STORAGE_VERSION,
|
|
57
|
+
private versionedBuckets: boolean = STORAGE_VERSION_CONFIG[storageVersion]?.versionedBuckets ?? false
|
|
49
58
|
) {
|
|
50
59
|
createCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
51
60
|
initializeCoreReplicationMetrics(METRICS_HELPER.metricsEngine);
|
|
@@ -95,7 +104,10 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
async updateSyncRules(content: string) {
|
|
98
|
-
const syncRules = await this.factory.updateSyncRules(
|
|
107
|
+
const syncRules = await this.factory.updateSyncRules(
|
|
108
|
+
updateSyncRulesFromYaml(content, { validate: true, storageVersion: this.storageVersion })
|
|
109
|
+
);
|
|
110
|
+
this.syncRulesId = syncRules.id;
|
|
99
111
|
this.storage = this.factory.getInstance(syncRules);
|
|
100
112
|
return this.storage!;
|
|
101
113
|
}
|
|
@@ -106,6 +118,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
106
118
|
throw new Error(`Next sync rules not available`);
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
this.syncRulesId = syncRules.id;
|
|
109
122
|
this.storage = this.factory.getInstance(syncRules);
|
|
110
123
|
return this.storage!;
|
|
111
124
|
}
|
|
@@ -116,6 +129,7 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
116
129
|
throw new Error(`Active sync rules not available`);
|
|
117
130
|
}
|
|
118
131
|
|
|
132
|
+
this.syncRulesId = syncRules.id;
|
|
119
133
|
this.storage = this.factory.getInstance(syncRules);
|
|
120
134
|
return this.storage!;
|
|
121
135
|
}
|
|
@@ -177,9 +191,21 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
177
191
|
return checkpoint;
|
|
178
192
|
}
|
|
179
193
|
|
|
194
|
+
private resolveBucketName(bucket: string) {
|
|
195
|
+
if (!this.versionedBuckets || /^\d+#/.test(bucket)) {
|
|
196
|
+
return bucket;
|
|
197
|
+
}
|
|
198
|
+
if (this.syncRulesId == null) {
|
|
199
|
+
throw new Error('Sync rules not configured - call updateSyncRules() first');
|
|
200
|
+
}
|
|
201
|
+
return `${this.syncRulesId}#${bucket}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
180
204
|
async getBucketsDataBatch(buckets: Record<string, InternalOpId>, options?: { timeout?: number }) {
|
|
181
205
|
let checkpoint = await this.getCheckpoint(options);
|
|
182
|
-
const map = new Map<string, InternalOpId>(
|
|
206
|
+
const map = new Map<string, InternalOpId>(
|
|
207
|
+
Object.entries(buckets).map(([bucket, opId]) => [this.resolveBucketName(bucket), opId])
|
|
208
|
+
);
|
|
183
209
|
return test_utils.fromAsync(this.storage!.getBucketDataBatch(checkpoint, map));
|
|
184
210
|
}
|
|
185
211
|
|
|
@@ -191,8 +217,9 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
191
217
|
if (typeof start == 'string') {
|
|
192
218
|
start = BigInt(start);
|
|
193
219
|
}
|
|
220
|
+
const resolvedBucket = this.resolveBucketName(bucket);
|
|
194
221
|
const checkpoint = await this.getCheckpoint(options);
|
|
195
|
-
const map = new Map<string, InternalOpId>([[
|
|
222
|
+
const map = new Map<string, InternalOpId>([[resolvedBucket, start]]);
|
|
196
223
|
let data: OplogEntry[] = [];
|
|
197
224
|
while (true) {
|
|
198
225
|
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
@@ -202,11 +229,29 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
202
229
|
if (batches.length == 0 || !batches[0]!.chunkData.has_more) {
|
|
203
230
|
break;
|
|
204
231
|
}
|
|
205
|
-
map.set(
|
|
232
|
+
map.set(resolvedBucket, BigInt(batches[0]!.chunkData.next_after));
|
|
206
233
|
}
|
|
207
234
|
return data;
|
|
208
235
|
}
|
|
209
236
|
|
|
237
|
+
async getChecksums(buckets: string[], options?: { timeout?: number }) {
|
|
238
|
+
const checkpoint = await this.getCheckpoint(options);
|
|
239
|
+
const versionedBuckets = buckets.map((bucket) => this.resolveBucketName(bucket));
|
|
240
|
+
const checksums = await this.storage!.getChecksums(checkpoint, versionedBuckets);
|
|
241
|
+
|
|
242
|
+
const unversioned = new Map();
|
|
243
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
244
|
+
unversioned.set(buckets[i], checksums.get(versionedBuckets[i])!);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return unversioned;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async getChecksum(bucket: string, options?: { timeout?: number }) {
|
|
251
|
+
const checksums = await this.getChecksums([bucket], options);
|
|
252
|
+
return checksums.get(bucket);
|
|
253
|
+
}
|
|
254
|
+
|
|
210
255
|
/**
|
|
211
256
|
* This does not wait for a client checkpoint.
|
|
212
257
|
*/
|
|
@@ -215,8 +260,9 @@ export class WalStreamTestContext implements AsyncDisposable {
|
|
|
215
260
|
if (typeof start == 'string') {
|
|
216
261
|
start = BigInt(start);
|
|
217
262
|
}
|
|
263
|
+
const resolvedBucket = this.resolveBucketName(bucket);
|
|
218
264
|
const { checkpoint } = await this.storage!.getCheckpoint();
|
|
219
|
-
const map = new Map<string, InternalOpId>([[
|
|
265
|
+
const map = new Map<string, InternalOpId>([[resolvedBucket, start]]);
|
|
220
266
|
const batch = this.storage!.getBucketDataBatch(checkpoint, map);
|
|
221
267
|
const batches = await test_utils.fromAsync(batch);
|
|
222
268
|
return batches[0]?.chunkData.data ?? [];
|
package/test/tsconfig.json
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "../../../tsconfig.
|
|
2
|
+
"extends": "../../../tsconfig.tests.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
4
|
"baseUrl": "./",
|
|
6
|
-
"noEmit": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"sourceMap": true,
|
|
10
5
|
"paths": {
|
|
11
6
|
"@/*": ["../../../packages/service-core/src/*"],
|
|
12
7
|
"@module/*": ["../src/*"],
|
|
13
8
|
"@core-tests/*": ["../../../packages/service-core/test/src/*"]
|
|
14
|
-
}
|
|
9
|
+
},
|
|
10
|
+
"rootDir": "src"
|
|
15
11
|
},
|
|
16
12
|
"include": ["src"],
|
|
17
13
|
"references": [
|