@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.
@@ -2,8 +2,7 @@ import { compareIds, putOp, reduceBucket, removeOp, test_utils } from '@powersyn
2
2
  import * as timers from 'timers/promises';
3
3
  import { describe, expect, test } from 'vitest';
4
4
 
5
- import { storage } from '@powersync/service-core';
6
- import { describeWithStorage } from './util.js';
5
+ import { describeWithStorage, StorageVersionTestContext } from './util.js';
7
6
  import { WalStreamTestContext } from './wal_stream_utils.js';
8
7
 
9
8
  describe('schema changes', { timeout: 20_000 }, function () {
@@ -24,9 +23,12 @@ const PUT_T3 = test_utils.putOp('test_data', { id: 't3', description: 'test3' })
24
23
  const REMOVE_T1 = test_utils.removeOp('test_data', 't1');
25
24
  const REMOVE_T2 = test_utils.removeOp('test_data', 't2');
26
25
 
27
- function defineTests(factory: storage.TestStorageFactory) {
26
+ function defineTests({ factory, storageVersion }: StorageVersionTestContext) {
27
+ const openContext = (options?: Parameters<typeof WalStreamTestContext.open>[1]) => {
28
+ return WalStreamTestContext.open(factory, { ...options, storageVersion });
29
+ };
28
30
  test('re-create table', async () => {
29
- await using context = await WalStreamTestContext.open(factory);
31
+ await using context = await openContext();
30
32
 
31
33
  // Drop a table and re-create it.
32
34
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -37,7 +39,6 @@ function defineTests(factory: storage.TestStorageFactory) {
37
39
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
38
40
 
39
41
  await context.replicateSnapshot();
40
- context.startStreaming();
41
42
 
42
43
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
43
44
 
@@ -60,23 +61,26 @@ function defineTests(factory: storage.TestStorageFactory) {
60
61
  // Truncate - order doesn't matter
61
62
  expect(data.slice(2, 4).sort(compareIds)).toMatchObject([REMOVE_T1, REMOVE_T2]);
62
63
 
63
- expect(data.slice(4)).toMatchObject([
64
- // Snapshot insert
65
- PUT_T3,
66
- // Replicated insert
67
- // We may eventually be able to de-duplicate this
64
+ expect(data.slice(4, 5)).toMatchObject([
65
+ // Snapshot and/or replication insert
68
66
  PUT_T3
69
67
  ]);
68
+
69
+ if (data.length > 5) {
70
+ expect(data.slice(5)).toMatchObject([
71
+ // Replicated insert (optional duplication)
72
+ PUT_T3
73
+ ]);
74
+ }
70
75
  });
71
76
 
72
77
  test('add table', async () => {
73
- await using context = await WalStreamTestContext.open(factory);
78
+ await using context = await openContext();
74
79
  // Add table after initial replication
75
80
  await context.updateSyncRules(BASIC_SYNC_RULES);
76
81
  const { pool } = context;
77
82
 
78
83
  await context.replicateSnapshot();
79
- context.startStreaming();
80
84
 
81
85
  await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
82
86
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
@@ -84,21 +88,14 @@ function defineTests(factory: storage.TestStorageFactory) {
84
88
  const data = await context.getBucketData('global[]');
85
89
 
86
90
  // "Reduce" the bucket to get a stable output to test.
91
+ // The specific operation sequence may vary depending on storage implementation, so just check the end result.
87
92
  // slice(1) to skip the CLEAR op.
88
93
  const reduced = reduceBucket(data).slice(1);
89
94
  expect(reduced.sort(compareIds)).toMatchObject([PUT_T1]);
90
-
91
- expect(data).toMatchObject([
92
- // Snapshot insert
93
- PUT_T1,
94
- // Replicated insert
95
- // We may eventually be able to de-duplicate this
96
- PUT_T1
97
- ]);
98
95
  });
99
96
 
100
97
  test('rename table (1)', async () => {
101
- await using context = await WalStreamTestContext.open(factory);
98
+ await using context = await openContext();
102
99
  const { pool } = context;
103
100
 
104
101
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -108,7 +105,6 @@ function defineTests(factory: storage.TestStorageFactory) {
108
105
  await pool.query(`INSERT INTO test_data_old(id, description) VALUES('t1', 'test1')`);
109
106
 
110
107
  await context.replicateSnapshot();
111
- context.startStreaming();
112
108
 
113
109
  await pool.query(
114
110
  { statement: `ALTER TABLE test_data_old RENAME TO test_data` },
@@ -128,15 +124,17 @@ function defineTests(factory: storage.TestStorageFactory) {
128
124
  PUT_T1,
129
125
  PUT_T2
130
126
  ]);
131
- expect(data.slice(2)).toMatchObject([
132
- // Replicated insert
133
- // We may eventually be able to de-duplicate this
134
- PUT_T2
135
- ]);
127
+ if (data.length > 2) {
128
+ expect(data.slice(2)).toMatchObject([
129
+ // Replicated insert
130
+ // May be de-duplicated
131
+ PUT_T2
132
+ ]);
133
+ }
136
134
  });
137
135
 
138
136
  test('rename table (2)', async () => {
139
- await using context = await WalStreamTestContext.open(factory);
137
+ await using context = await openContext();
140
138
  // Rename table in sync rules -> in sync rules
141
139
  const { pool } = context;
142
140
 
@@ -151,7 +149,6 @@ function defineTests(factory: storage.TestStorageFactory) {
151
149
  await pool.query(`INSERT INTO test_data1(id, description) VALUES('t1', 'test1')`);
152
150
 
153
151
  await context.replicateSnapshot();
154
- context.startStreaming();
155
152
 
156
153
  await pool.query(
157
154
  { statement: `ALTER TABLE test_data1 RENAME TO test_data2` },
@@ -181,15 +178,17 @@ function defineTests(factory: storage.TestStorageFactory) {
181
178
  putOp('test_data2', { id: 't1', description: 'test1' }),
182
179
  putOp('test_data2', { id: 't2', description: 'test2' })
183
180
  ]);
184
- expect(data.slice(4)).toMatchObject([
185
- // Replicated insert
186
- // We may eventually be able to de-duplicate this
187
- putOp('test_data2', { id: 't2', description: 'test2' })
188
- ]);
181
+ if (data.length > 4) {
182
+ expect(data.slice(4)).toMatchObject([
183
+ // Replicated insert
184
+ // This may be de-duplicated
185
+ putOp('test_data2', { id: 't2', description: 'test2' })
186
+ ]);
187
+ }
189
188
  });
190
189
 
191
190
  test('rename table (3)', async () => {
192
- await using context = await WalStreamTestContext.open(factory);
191
+ await using context = await openContext();
193
192
  // Rename table in sync rules -> not in sync rules
194
193
 
195
194
  const { pool } = context;
@@ -200,7 +199,6 @@ function defineTests(factory: storage.TestStorageFactory) {
200
199
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
201
200
 
202
201
  await context.replicateSnapshot();
203
- context.startStreaming();
204
202
 
205
203
  await pool.query(
206
204
  { statement: `ALTER TABLE test_data RENAME TO test_data_na` },
@@ -224,7 +222,7 @@ function defineTests(factory: storage.TestStorageFactory) {
224
222
  });
225
223
 
226
224
  test('change replica id', async () => {
227
- await using context = await WalStreamTestContext.open(factory);
225
+ await using context = await openContext();
228
226
  // Change replica id from default to full
229
227
  // Causes a re-import of the table.
230
228
 
@@ -235,7 +233,6 @@ function defineTests(factory: storage.TestStorageFactory) {
235
233
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
236
234
 
237
235
  await context.replicateSnapshot();
238
- context.startStreaming();
239
236
 
240
237
  await pool.query(
241
238
  { statement: `ALTER TABLE test_data REPLICA IDENTITY FULL` },
@@ -260,15 +257,17 @@ function defineTests(factory: storage.TestStorageFactory) {
260
257
  // Snapshot - order doesn't matter
261
258
  expect(data.slice(2, 4).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
262
259
 
263
- expect(data.slice(4).sort(compareIds)).toMatchObject([
264
- // Replicated insert
265
- // We may eventually be able to de-duplicate this
266
- PUT_T2
267
- ]);
260
+ if (data.length > 4) {
261
+ expect(data.slice(4).sort(compareIds)).toMatchObject([
262
+ // Replicated insert
263
+ // This may be de-duplicated
264
+ PUT_T2
265
+ ]);
266
+ }
268
267
  });
269
268
 
270
269
  test('change full replica id by adding column', async () => {
271
- await using context = await WalStreamTestContext.open(factory);
270
+ await using context = await openContext();
272
271
  // Change replica id from full by adding column
273
272
  // Causes a re-import of the table.
274
273
  // Other changes such as renaming column would have the same effect
@@ -281,7 +280,6 @@ function defineTests(factory: storage.TestStorageFactory) {
281
280
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
282
281
 
283
282
  await context.replicateSnapshot();
284
- context.startStreaming();
285
283
 
286
284
  await pool.query(
287
285
  { statement: `ALTER TABLE test_data ADD COLUMN other TEXT` },
@@ -303,15 +301,17 @@ function defineTests(factory: storage.TestStorageFactory) {
303
301
  putOp('test_data', { id: 't2', description: 'test2', other: null })
304
302
  ]);
305
303
 
306
- expect(data.slice(4).sort(compareIds)).toMatchObject([
307
- // Replicated insert
308
- // We may eventually be able to de-duplicate this
309
- putOp('test_data', { id: 't2', description: 'test2', other: null })
310
- ]);
304
+ if (data.length > 4) {
305
+ expect(data.slice(4).sort(compareIds)).toMatchObject([
306
+ // Replicated insert
307
+ // This may be de-duplicated
308
+ putOp('test_data', { id: 't2', description: 'test2', other: null })
309
+ ]);
310
+ }
311
311
  });
312
312
 
313
313
  test('change default replica id by changing column type', async () => {
314
- await using context = await WalStreamTestContext.open(factory);
314
+ await using context = await openContext();
315
315
  // Change default replica id by changing column type
316
316
  // Causes a re-import of the table.
317
317
  const { pool } = context;
@@ -321,7 +321,6 @@ function defineTests(factory: storage.TestStorageFactory) {
321
321
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
322
322
 
323
323
  await context.replicateSnapshot();
324
- context.startStreaming();
325
324
 
326
325
  await pool.query(
327
326
  { statement: `ALTER TABLE test_data ALTER COLUMN id TYPE varchar` },
@@ -340,15 +339,17 @@ function defineTests(factory: storage.TestStorageFactory) {
340
339
  // Snapshot - order doesn't matter
341
340
  expect(data.slice(2, 4).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
342
341
 
343
- expect(data.slice(4).sort(compareIds)).toMatchObject([
344
- // Replicated insert
345
- // We may eventually be able to de-duplicate this
346
- PUT_T2
347
- ]);
342
+ if (data.length > 4) {
343
+ expect(data.slice(4).sort(compareIds)).toMatchObject([
344
+ // Replicated insert
345
+ // May be de-duplicated
346
+ PUT_T2
347
+ ]);
348
+ }
348
349
  });
349
350
 
350
351
  test('change index id by changing column type', async () => {
351
- await using context = await WalStreamTestContext.open(factory);
352
+ await using context = await openContext();
352
353
  // Change index replica id by changing column type
353
354
  // Causes a re-import of the table.
354
355
  // Secondary functionality tested here is that replica id column order stays
@@ -363,7 +364,6 @@ function defineTests(factory: storage.TestStorageFactory) {
363
364
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
364
365
 
365
366
  await context.replicateSnapshot();
366
- context.startStreaming();
367
367
 
368
368
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
369
369
 
@@ -386,25 +386,11 @@ function defineTests(factory: storage.TestStorageFactory) {
386
386
  const reduced = reduceBucket(data).slice(1);
387
387
  expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2, PUT_T3]);
388
388
 
389
- // Previously had more specific tests, but this varies too much based on timing:
390
- // expect(data.slice(2, 4).sort(compareIds)).toMatchObject([
391
- // // Truncate - any order
392
- // REMOVE_T1,
393
- // REMOVE_T2
394
- // ]);
395
-
396
- // // Snapshot - order doesn't matter
397
- // expect(data.slice(4, 7).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2, PUT_T3]);
398
-
399
- // expect(data.slice(7).sort(compareIds)).toMatchObject([
400
- // // Replicated insert
401
- // // We may eventually be able to de-duplicate this
402
- // PUT_T3
403
- // ]);
389
+ // Previously had more specific tests, but this varies too much based on timing.
404
390
  });
405
391
 
406
392
  test('add to publication', async () => {
407
- await using context = await WalStreamTestContext.open(factory);
393
+ await using context = await openContext();
408
394
  // Add table to publication after initial replication
409
395
  const { pool } = context;
410
396
 
@@ -418,7 +404,6 @@ function defineTests(factory: storage.TestStorageFactory) {
418
404
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
419
405
 
420
406
  await context.replicateSnapshot();
421
- context.startStreaming();
422
407
 
423
408
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
424
409
 
@@ -434,11 +419,13 @@ function defineTests(factory: storage.TestStorageFactory) {
434
419
  PUT_T3
435
420
  ]);
436
421
 
437
- expect(data.slice(3)).toMatchObject([
438
- // Replicated insert
439
- // We may eventually be able to de-duplicate this
440
- PUT_T3
441
- ]);
422
+ if (data.length > 3) {
423
+ expect(data.slice(3)).toMatchObject([
424
+ // Replicated insert
425
+ // May be de-duplicated
426
+ PUT_T3
427
+ ]);
428
+ }
442
429
 
443
430
  // "Reduce" the bucket to get a stable output to test.
444
431
  // slice(1) to skip the CLEAR op.
@@ -447,7 +434,7 @@ function defineTests(factory: storage.TestStorageFactory) {
447
434
  });
448
435
 
449
436
  test('add to publication (not in sync rules)', async () => {
450
- await using context = await WalStreamTestContext.open(factory);
437
+ await using context = await openContext();
451
438
  // Add table to publication after initial replication
452
439
  // Since the table is not in sync rules, it should not be replicated.
453
440
  const { pool } = context;
@@ -462,7 +449,6 @@ function defineTests(factory: storage.TestStorageFactory) {
462
449
  await pool.query(`INSERT INTO test_other(id, description) VALUES('t1', 'test1')`);
463
450
 
464
451
  await context.replicateSnapshot();
465
- context.startStreaming();
466
452
 
467
453
  await pool.query(`INSERT INTO test_other(id, description) VALUES('t2', 'test2')`);
468
454
 
@@ -474,7 +460,7 @@ function defineTests(factory: storage.TestStorageFactory) {
474
460
  });
475
461
 
476
462
  test('replica identity nothing', async () => {
477
- await using context = await WalStreamTestContext.open(factory);
463
+ await using context = await openContext();
478
464
  // Technically not a schema change, but fits here.
479
465
  // Replica ID works a little differently here - the table doesn't have
480
466
  // one defined, but we generate a unique one for each replicated row.
@@ -487,7 +473,6 @@ function defineTests(factory: storage.TestStorageFactory) {
487
473
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
488
474
 
489
475
  await context.replicateSnapshot();
490
- context.startStreaming();
491
476
 
492
477
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
493
478
 
@@ -521,7 +506,7 @@ function defineTests(factory: storage.TestStorageFactory) {
521
506
  });
522
507
 
523
508
  test('replica identity default without PK', async () => {
524
- await using context = await WalStreamTestContext.open(factory);
509
+ await using context = await openContext();
525
510
  // Same as no replica identity
526
511
  const { pool } = context;
527
512
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -530,7 +515,6 @@ function defineTests(factory: storage.TestStorageFactory) {
530
515
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
531
516
 
532
517
  await context.replicateSnapshot();
533
- context.startStreaming();
534
518
 
535
519
  await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
536
520
 
@@ -573,7 +557,7 @@ function defineTests(factory: storage.TestStorageFactory) {
573
557
  // await new Promise((resolve) => setTimeout(resolve, 100));
574
558
  // await this.snapshotTable(batch, db, result.table);
575
559
  test('table snapshot consistency', async () => {
576
- await using context = await WalStreamTestContext.open(factory);
560
+ await using context = await openContext();
577
561
  const { pool } = context;
578
562
 
579
563
  await context.updateSyncRules(BASIC_SYNC_RULES);
@@ -584,7 +568,6 @@ function defineTests(factory: storage.TestStorageFactory) {
584
568
  await pool.query(`INSERT INTO test_data_old(id, num) VALUES('t2', 0)`);
585
569
 
586
570
  await context.replicateSnapshot();
587
- context.startStreaming();
588
571
 
589
572
  await pool.query(
590
573
  { statement: `ALTER TABLE test_data_old RENAME TO test_data` },
@@ -640,7 +623,7 @@ function defineTests(factory: storage.TestStorageFactory) {
640
623
  });
641
624
 
642
625
  test('custom types', async () => {
643
- await using context = await WalStreamTestContext.open(factory);
626
+ await using context = await openContext();
644
627
 
645
628
  await context.updateSyncRules(`
646
629
  streams:
@@ -656,7 +639,6 @@ config:
656
639
  await pool.query(`INSERT INTO test_data(id) VALUES ('t1')`);
657
640
 
658
641
  await context.replicateSnapshot();
659
- context.startStreaming();
660
642
 
661
643
  await pool.query(
662
644
  { statement: `CREATE TYPE composite AS (foo bool, bar int4);` },
@@ -664,7 +646,7 @@ config:
664
646
  { statement: `UPDATE test_data SET other = ROW(TRUE, 2)::composite;` }
665
647
  );
666
648
 
667
- const data = await context.getBucketData('1#stream|0[]');
649
+ const data = await context.getBucketData('stream|0[]');
668
650
  expect(data).toMatchObject([
669
651
  putOp('test_data', { id: 't1' }),
670
652
  putOp('test_data', { id: 't1', other: '{"foo":1,"bar":2}' })