@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
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
import { reduceBucket
|
|
1
|
+
import { reduceBucket } from '@powersync/service-core';
|
|
2
2
|
import { METRICS_HELPER } from '@powersync/service-core-tests';
|
|
3
3
|
import { SqliteJsonValue } from '@powersync/service-sync-rules';
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
5
|
import * as timers from 'timers/promises';
|
|
6
6
|
import { describe, expect, test } from 'vitest';
|
|
7
|
-
import { describeWithStorage } from './util.js';
|
|
7
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
8
8
|
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
9
9
|
|
|
10
10
|
describe('chunked snapshots', () => {
|
|
11
11
|
describeWithStorage({ timeout: 120_000 }, defineBatchTests);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
function defineBatchTests(factory:
|
|
14
|
+
function defineBatchTests({ factory, storageVersion }: StorageVersionTestContext) {
|
|
15
|
+
const openContext = (options?: Parameters<typeof WalStreamTestContext.open>[1]) => {
|
|
16
|
+
return WalStreamTestContext.open(factory, { ...options, storageVersion });
|
|
17
|
+
};
|
|
18
|
+
|
|
15
19
|
// We need to test every supported type, since chunking could be quite sensitive to
|
|
16
20
|
// how each specific type is handled.
|
|
17
21
|
test('chunked snapshot edge case (int2)', async () => {
|
|
@@ -89,7 +93,7 @@ function defineBatchTests(factory: TestStorageFactory) {
|
|
|
89
93
|
// 5. Logical replication picks up the UPDATE above, but it is missing the TOAST column.
|
|
90
94
|
// 6. We end up with a row that has a missing TOAST column.
|
|
91
95
|
|
|
92
|
-
await using context = await
|
|
96
|
+
await using context = await openContext({
|
|
93
97
|
// We need to use a smaller chunk size here, so that we can run a query in between chunks
|
|
94
98
|
walStreamOptions: { snapshotChunkLength: 100 }
|
|
95
99
|
});
|
|
@@ -142,7 +146,8 @@ function defineBatchTests(factory: TestStorageFactory) {
|
|
|
142
146
|
await p;
|
|
143
147
|
|
|
144
148
|
// 5. Logical replication picks up the UPDATE above, but it is missing the TOAST column.
|
|
145
|
-
|
|
149
|
+
// Note: logical replication now runs concurrently with the snapshot.
|
|
150
|
+
// TODO: re-check the test logic here.
|
|
146
151
|
|
|
147
152
|
// 6. If all went well, the "resnapshot" process would take care of this.
|
|
148
153
|
const data = await context.getBucketData('global[]', undefined, {});
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { storage } from '@powersync/service-core';
|
|
2
1
|
import { describe, expect, test } from 'vitest';
|
|
3
2
|
import { populateData } from '../../dist/utils/populate_test_data.js';
|
|
4
3
|
import { env } from './env.js';
|
|
5
|
-
import { describeWithStorage, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
4
|
+
import { describeWithStorage, StorageVersionTestContext, TEST_CONNECTION_OPTIONS } from './util.js';
|
|
6
5
|
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
7
6
|
|
|
8
7
|
describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
|
|
9
|
-
describeWithStorage({ timeout: 240_000 },
|
|
10
|
-
defineBatchTests(factory);
|
|
11
|
-
});
|
|
8
|
+
describeWithStorage({ timeout: 240_000 }, defineBatchTests);
|
|
12
9
|
});
|
|
13
10
|
|
|
14
11
|
const BASIC_SYNC_RULES = `bucket_definitions:
|
|
@@ -16,9 +13,13 @@ const BASIC_SYNC_RULES = `bucket_definitions:
|
|
|
16
13
|
data:
|
|
17
14
|
- SELECT id, description, other FROM "test_data"`;
|
|
18
15
|
|
|
19
|
-
function defineBatchTests(factory:
|
|
16
|
+
function defineBatchTests({ factory, storageVersion }: StorageVersionTestContext) {
|
|
17
|
+
const openContext = (options?: Parameters<typeof WalStreamTestContext.open>[1]) => {
|
|
18
|
+
return WalStreamTestContext.open(factory, { ...options, storageVersion });
|
|
19
|
+
};
|
|
20
|
+
|
|
20
21
|
test('update large record', async () => {
|
|
21
|
-
await using context = await
|
|
22
|
+
await using context = await openContext();
|
|
22
23
|
// This test generates a large transaction in MongoDB, despite the replicated data
|
|
23
24
|
// not being that large.
|
|
24
25
|
// If we don't limit transaction size, we could run into this error:
|
|
@@ -39,19 +40,16 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
39
40
|
|
|
40
41
|
const start = Date.now();
|
|
41
42
|
|
|
42
|
-
context.
|
|
43
|
-
|
|
44
|
-
const checkpoint = await context.getCheckpoint({ timeout: 100_000 });
|
|
43
|
+
const checksum = await context.getChecksums(['global[]'], { timeout: 100_000 });
|
|
45
44
|
const duration = Date.now() - start;
|
|
46
45
|
const used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
47
|
-
const checksum = await context.storage!.getChecksums(checkpoint, ['global[]']);
|
|
48
46
|
expect(checksum.get('global[]')!.count).toEqual(operation_count);
|
|
49
47
|
const perSecond = Math.round((operation_count / duration) * 1000);
|
|
50
48
|
console.log(`${operation_count} ops in ${duration}ms ${perSecond} ops/s. ${used}MB heap`);
|
|
51
49
|
});
|
|
52
50
|
|
|
53
51
|
test('initial replication performance', async () => {
|
|
54
|
-
await using context = await
|
|
52
|
+
await using context = await openContext();
|
|
55
53
|
// Manual test to check initial replication performance and memory usage
|
|
56
54
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
57
55
|
const { pool } = context;
|
|
@@ -87,11 +85,9 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
87
85
|
const start = Date.now();
|
|
88
86
|
|
|
89
87
|
await context.replicateSnapshot();
|
|
90
|
-
context.startStreaming();
|
|
91
88
|
|
|
92
|
-
const
|
|
89
|
+
const checksum = await context.getChecksums(['global[]'], { timeout: 100_000 });
|
|
93
90
|
const duration = Date.now() - start;
|
|
94
|
-
const checksum = await context.storage!.getChecksums(checkpoint, ['global[]']);
|
|
95
91
|
expect(checksum.get('global[]')!.count).toEqual(operation_count);
|
|
96
92
|
const perSecond = Math.round((operation_count / duration) * 1000);
|
|
97
93
|
console.log(`${operation_count} ops in ${duration}ms ${perSecond} ops/s.`);
|
|
@@ -102,7 +98,7 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
102
98
|
});
|
|
103
99
|
|
|
104
100
|
test('large number of operations', async () => {
|
|
105
|
-
await using context = await
|
|
101
|
+
await using context = await openContext();
|
|
106
102
|
// This just tests performance of a large number of operations inside a transaction.
|
|
107
103
|
await context.updateSyncRules(BASIC_SYNC_RULES);
|
|
108
104
|
const { pool } = context;
|
|
@@ -139,12 +135,9 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
139
135
|
|
|
140
136
|
const start = Date.now();
|
|
141
137
|
|
|
142
|
-
context.
|
|
143
|
-
|
|
144
|
-
const checkpoint = await context.getCheckpoint({ timeout: 50_000 });
|
|
138
|
+
const checksum = await context.getChecksums(['global[]']);
|
|
145
139
|
const duration = Date.now() - start;
|
|
146
140
|
const used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
147
|
-
const checksum = await context.storage!.getChecksums(checkpoint, ['global[]']);
|
|
148
141
|
expect(checksum.get('global[]')!.count).toEqual(operationCount);
|
|
149
142
|
const perSecond = Math.round((operationCount / duration) * 1000);
|
|
150
143
|
// This number depends on the test machine, so we keep the test significantly
|
|
@@ -158,10 +151,8 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
158
151
|
const truncateStart = Date.now();
|
|
159
152
|
await pool.query(`TRUNCATE test_data`);
|
|
160
153
|
|
|
161
|
-
const
|
|
154
|
+
const checksum2 = await context.getChecksums(['global[]'], { timeout: 20_000 });
|
|
162
155
|
const truncateDuration = Date.now() - truncateStart;
|
|
163
|
-
|
|
164
|
-
const checksum2 = await context.storage!.getChecksums(checkpoint2, ['global[]']);
|
|
165
156
|
const truncateCount = checksum2.get('global[]')!.count - checksum.get('global[]')!.count;
|
|
166
157
|
expect(truncateCount).toEqual(numTransactions * perTransaction);
|
|
167
158
|
const truncatePerSecond = Math.round((truncateCount / truncateDuration) * 1000);
|
|
@@ -183,7 +174,7 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
183
174
|
// 4. Another document to make sure the internal batching overflows
|
|
184
175
|
// to a second batch.
|
|
185
176
|
|
|
186
|
-
await using context = await
|
|
177
|
+
await using context = await openContext();
|
|
187
178
|
await context.updateSyncRules(`bucket_definitions:
|
|
188
179
|
global:
|
|
189
180
|
data:
|
|
@@ -226,10 +217,7 @@ function defineBatchTests(factory: storage.TestStorageFactory) {
|
|
|
226
217
|
});
|
|
227
218
|
await context.replicateSnapshot();
|
|
228
219
|
|
|
229
|
-
context.
|
|
230
|
-
|
|
231
|
-
const checkpoint = await context.getCheckpoint({ timeout: 50_000 });
|
|
232
|
-
const checksum = await context.storage!.getChecksums(checkpoint, ['global[]']);
|
|
220
|
+
const checksum = await context.getChecksums(['global[]'], { timeout: 50_000 });
|
|
233
221
|
expect(checksum.get('global[]')!.count).toEqual((numDocs + 2) * 4);
|
|
234
222
|
});
|
|
235
223
|
|
package/test/src/pg_test.test.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { WalStream } from '@module/replication/WalStream.js';
|
|
2
|
+
import { PostgresTypeResolver } from '@module/types/resolver.js';
|
|
2
3
|
import * as dns from 'node:dns';
|
|
4
|
+
import type { LookupFunction } from 'node:net';
|
|
3
5
|
|
|
4
6
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
5
7
|
import {
|
|
6
8
|
applyRowContext,
|
|
7
9
|
CompatibilityContext,
|
|
8
|
-
|
|
10
|
+
CompatibilityEdition,
|
|
9
11
|
DateTimeValue,
|
|
12
|
+
SqliteInputRow,
|
|
10
13
|
TimeValue,
|
|
11
|
-
CompatibilityEdition,
|
|
12
14
|
TimeValuePrecision
|
|
13
15
|
} from '@powersync/service-sync-rules';
|
|
14
16
|
import { describe, expect, Mock, test, vi } from 'vitest';
|
|
15
17
|
import { clearTestDb, connectPgPool, connectPgWire, TEST_CONNECTION_OPTIONS, TEST_URI } from './util.js';
|
|
16
|
-
import { WalStream } from '@module/replication/WalStream.js';
|
|
17
|
-
import { PostgresTypeResolver } from '@module/types/resolver.js';
|
|
18
18
|
|
|
19
19
|
describe('connection options', () => {
|
|
20
20
|
test('uses custom lookup', async () => {
|
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { env } from './env.js';
|
|
3
|
-
import { describeWithStorage } from './util.js';
|
|
4
|
-
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
5
|
-
import { TestStorageFactory } from '@powersync/service-core';
|
|
6
1
|
import { METRICS_HELPER } from '@powersync/service-core-tests';
|
|
7
2
|
import { ReplicationMetric } from '@powersync/service-types';
|
|
8
3
|
import * as timers from 'node:timers/promises';
|
|
9
|
-
import {
|
|
4
|
+
import { describe, expect, test } from 'vitest';
|
|
5
|
+
import { env } from './env.js';
|
|
6
|
+
import { describeWithStorage, StorageVersionTestContext } from './util.js';
|
|
7
|
+
import { WalStreamTestContext } from './wal_stream_utils.js';
|
|
10
8
|
|
|
11
9
|
describe.skipIf(!(env.CI || env.SLOW_TESTS))('batch replication', function () {
|
|
12
|
-
describeWithStorage({ timeout: 240_000 }, function (factory) {
|
|
10
|
+
describeWithStorage({ timeout: 240_000 }, function ({ factory, storageVersion }) {
|
|
13
11
|
test('resuming initial replication (1)', async () => {
|
|
14
12
|
// Stop early - likely to not include deleted row in first replication attempt.
|
|
15
|
-
await testResumingReplication(factory, 2000);
|
|
13
|
+
await testResumingReplication(factory, storageVersion, 2000);
|
|
16
14
|
});
|
|
17
15
|
test('resuming initial replication (2)', async () => {
|
|
18
16
|
// Stop late - likely to include deleted row in first replication attempt.
|
|
19
|
-
await testResumingReplication(factory, 8000);
|
|
17
|
+
await testResumingReplication(factory, storageVersion, 8000);
|
|
20
18
|
});
|
|
21
19
|
});
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
async function testResumingReplication(
|
|
22
|
+
async function testResumingReplication(
|
|
23
|
+
factory: StorageVersionTestContext['factory'],
|
|
24
|
+
storageVersion: number,
|
|
25
|
+
stopAfter: number
|
|
26
|
+
) {
|
|
25
27
|
// This tests interrupting and then resuming initial replication.
|
|
26
28
|
// We interrupt replication after test_data1 has fully replicated, and
|
|
27
29
|
// test_data2 has partially replicated.
|
|
@@ -33,7 +35,10 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
33
35
|
// have been / have not been replicated at that point is not deterministic.
|
|
34
36
|
// We do allow for some variation in the test results to account for this.
|
|
35
37
|
|
|
36
|
-
await using context = await WalStreamTestContext.open(factory, {
|
|
38
|
+
await using context = await WalStreamTestContext.open(factory, {
|
|
39
|
+
storageVersion,
|
|
40
|
+
walStreamOptions: { snapshotChunkLength: 1000 }
|
|
41
|
+
});
|
|
37
42
|
|
|
38
43
|
await context.updateSyncRules(`bucket_definitions:
|
|
39
44
|
global:
|
|
@@ -74,8 +79,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
74
79
|
await context.dispose();
|
|
75
80
|
})();
|
|
76
81
|
// This confirms that initial replication was interrupted
|
|
77
|
-
|
|
78
|
-
expect(error).toBeInstanceOf(ReplicationAbortedError);
|
|
82
|
+
await expect(p).rejects.toThrowError();
|
|
79
83
|
done = true;
|
|
80
84
|
} finally {
|
|
81
85
|
done = true;
|
|
@@ -84,6 +88,7 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
84
88
|
// Bypass the usual "clear db on factory open" step.
|
|
85
89
|
await using context2 = await WalStreamTestContext.open(factory, {
|
|
86
90
|
doNotClear: true,
|
|
91
|
+
storageVersion,
|
|
87
92
|
walStreamOptions: { snapshotChunkLength: 1000 }
|
|
88
93
|
});
|
|
89
94
|
|
|
@@ -104,7 +109,6 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
104
109
|
await context2.loadNextSyncRules();
|
|
105
110
|
await context2.replicateSnapshot();
|
|
106
111
|
|
|
107
|
-
context2.startStreaming();
|
|
108
112
|
const data = await context2.getBucketData('global[]', undefined, {});
|
|
109
113
|
|
|
110
114
|
const deletedRowOps = data.filter(
|
|
@@ -127,14 +131,14 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
127
131
|
// so it's not in the resulting ops at all.
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
expect(updatedRowOps.length).
|
|
134
|
+
expect(updatedRowOps.length).toBeGreaterThanOrEqual(2);
|
|
131
135
|
// description for the first op could be 'foo' or 'update1'.
|
|
132
136
|
// We only test the final version.
|
|
133
|
-
expect(JSON.parse(updatedRowOps[1].data as string).description).toEqual('update1');
|
|
137
|
+
expect(JSON.parse(updatedRowOps[updatedRowOps.length - 1].data as string).description).toEqual('update1');
|
|
134
138
|
|
|
135
|
-
expect(insertedRowOps.length).
|
|
139
|
+
expect(insertedRowOps.length).toBeGreaterThanOrEqual(1);
|
|
136
140
|
expect(JSON.parse(insertedRowOps[0].data as string).description).toEqual('insert1');
|
|
137
|
-
expect(JSON.parse(insertedRowOps[1].data as string).description).toEqual('insert1');
|
|
141
|
+
expect(JSON.parse(insertedRowOps[insertedRowOps.length - 1].data as string).description).toEqual('insert1');
|
|
138
142
|
|
|
139
143
|
// 1000 of test_data1 during first replication attempt.
|
|
140
144
|
// N >= 1000 of test_data2 during first replication attempt.
|
|
@@ -145,12 +149,12 @@ async function testResumingReplication(factory: TestStorageFactory, stopAfter: n
|
|
|
145
149
|
// This adds 2 ops.
|
|
146
150
|
// We expect this to be 11002 for stopAfter: 2000, and 11004 for stopAfter: 8000.
|
|
147
151
|
// However, this is not deterministic.
|
|
148
|
-
const expectedCount =
|
|
152
|
+
const expectedCount = 11000 - 2 + insertedRowOps.length + updatedRowOps.length + deletedRowOps.length;
|
|
149
153
|
expect(data.length).toEqual(expectedCount);
|
|
150
154
|
|
|
151
155
|
const replicatedCount =
|
|
152
156
|
((await METRICS_HELPER.getMetricValueForTests(ReplicationMetric.ROWS_REPLICATED)) ?? 0) - startRowCount;
|
|
153
157
|
|
|
154
158
|
// With resumable replication, there should be no need to re-replicate anything.
|
|
155
|
-
expect(replicatedCount).
|
|
159
|
+
expect(replicatedCount).toBeGreaterThanOrEqual(expectedCount);
|
|
156
160
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { clearTestDb, connectPgPool } from './util.js';
|
|
3
1
|
import { PostgresRouteAPIAdapter } from '@module/api/PostgresRouteAPIAdapter.js';
|
|
4
2
|
import { TYPE_INTEGER, TYPE_REAL, TYPE_TEXT } from '@powersync/service-sync-rules';
|
|
3
|
+
import { describe, expect, test } from 'vitest';
|
|
4
|
+
import { clearTestDb, connectPgPool } from './util.js';
|
|
5
5
|
|
|
6
6
|
describe('PostgresRouteAPIAdapter tests', () => {
|
|
7
7
|
test('infers connection schema', async () => {
|
|
@@ -20,7 +20,9 @@ describe('PostgresRouteAPIAdapter tests', () => {
|
|
|
20
20
|
`);
|
|
21
21
|
|
|
22
22
|
const schema = await api.getConnectionSchema();
|
|
23
|
-
|
|
23
|
+
// Ignore any other potential schemas in the test database, for example the 'powersync' schema.
|
|
24
|
+
const filtered = schema.filter((s) => s.name == 'public');
|
|
25
|
+
expect(filtered).toStrictEqual([
|
|
24
26
|
{
|
|
25
27
|
name: 'public',
|
|
26
28
|
tables: [
|