@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.
@@ -1,17 +1,21 @@
1
- import { reduceBucket, TestStorageFactory } from '@powersync/service-core';
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: TestStorageFactory) {
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 WalStreamTestContext.open(factory, {
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
- context.startStreaming();
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 }, function (factory) {
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: storage.TestStorageFactory) {
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 WalStreamTestContext.open(factory);
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.startStreaming();
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 WalStreamTestContext.open(factory);
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 checkpoint = await context.getCheckpoint({ timeout: 100_000 });
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 WalStreamTestContext.open(factory);
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.startStreaming();
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 checkpoint2 = await context.getCheckpoint({ timeout: 20_000 });
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 WalStreamTestContext.open(factory);
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.startStreaming();
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
 
@@ -1,20 +1,20 @@
1
- import type { LookupFunction } from 'node:net';
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
- SqliteInputRow,
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 { ReplicationAbortedError } from '@powersync/lib-services-framework';
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(factory: TestStorageFactory, stopAfter: number) {
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, { walStreamOptions: { snapshotChunkLength: 1000 } });
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
- const error = await p.catch((e) => e);
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).toEqual(2);
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).toEqual(2);
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 = 11002 + deletedRowOps.length;
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).toEqual(expectedCount);
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
- expect(schema).toStrictEqual([
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: [