@powersync/service-module-postgres 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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.
Files changed (84) hide show
  1. package/dist/api/PostgresRouteAPIAdapter.d.ts +1 -1
  2. package/dist/api/PostgresRouteAPIAdapter.js +63 -72
  3. package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
  4. package/dist/module/PostgresModule.js.map +1 -1
  5. package/dist/replication/MissingReplicationSlotError.d.ts +41 -0
  6. package/dist/replication/MissingReplicationSlotError.js +33 -0
  7. package/dist/replication/MissingReplicationSlotError.js.map +1 -0
  8. package/dist/replication/PgManager.js +3 -2
  9. package/dist/replication/PgManager.js.map +1 -1
  10. package/dist/replication/PostgresErrorRateLimiter.js +1 -1
  11. package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
  12. package/dist/replication/SnapshotQuery.js +8 -7
  13. package/dist/replication/SnapshotQuery.js.map +1 -1
  14. package/dist/replication/WalStream.d.ts +46 -16
  15. package/dist/replication/WalStream.js +203 -69
  16. package/dist/replication/WalStream.js.map +1 -1
  17. package/dist/replication/WalStreamReplicationJob.d.ts +1 -1
  18. package/dist/replication/WalStreamReplicationJob.js +9 -6
  19. package/dist/replication/WalStreamReplicationJob.js.map +1 -1
  20. package/dist/replication/WalStreamReplicator.d.ts +0 -1
  21. package/dist/replication/WalStreamReplicator.js +0 -22
  22. package/dist/replication/WalStreamReplicator.js.map +1 -1
  23. package/dist/replication/replication-index.d.ts +3 -1
  24. package/dist/replication/replication-index.js +3 -1
  25. package/dist/replication/replication-index.js.map +1 -1
  26. package/dist/replication/replication-utils.d.ts +3 -11
  27. package/dist/replication/replication-utils.js +103 -165
  28. package/dist/replication/replication-utils.js.map +1 -1
  29. package/dist/replication/rquery.d.ts +5 -0
  30. package/dist/replication/rquery.js +35 -0
  31. package/dist/replication/rquery.js.map +1 -0
  32. package/dist/replication/wal-budget-utils.d.ts +23 -0
  33. package/dist/replication/wal-budget-utils.js +57 -0
  34. package/dist/replication/wal-budget-utils.js.map +1 -0
  35. package/dist/types/registry.js +1 -1
  36. package/dist/types/registry.js.map +1 -1
  37. package/dist/utils/errors.d.ts +2 -0
  38. package/dist/utils/errors.js +30 -0
  39. package/dist/utils/errors.js.map +1 -0
  40. package/package.json +17 -13
  41. package/sql/check-source-configuration.plpgsql +13 -0
  42. package/sql/debug-tables-info-batched.plpgsql +230 -0
  43. package/CHANGELOG.md +0 -794
  44. package/src/api/PostgresRouteAPIAdapter.ts +0 -356
  45. package/src/index.ts +0 -1
  46. package/src/module/PostgresModule.ts +0 -122
  47. package/src/replication/ConnectionManagerFactory.ts +0 -33
  48. package/src/replication/PgManager.ts +0 -122
  49. package/src/replication/PgRelation.ts +0 -41
  50. package/src/replication/PostgresErrorRateLimiter.ts +0 -48
  51. package/src/replication/SnapshotQuery.ts +0 -213
  52. package/src/replication/WalStream.ts +0 -1138
  53. package/src/replication/WalStreamReplicationJob.ts +0 -138
  54. package/src/replication/WalStreamReplicator.ts +0 -79
  55. package/src/replication/replication-index.ts +0 -5
  56. package/src/replication/replication-utils.ts +0 -398
  57. package/src/types/registry.ts +0 -275
  58. package/src/types/resolver.ts +0 -227
  59. package/src/types/types.ts +0 -44
  60. package/src/utils/application-name.ts +0 -8
  61. package/src/utils/migration_lib.ts +0 -80
  62. package/src/utils/populate_test_data.ts +0 -37
  63. package/src/utils/populate_test_data_worker.ts +0 -53
  64. package/src/utils/postgres_version.ts +0 -8
  65. package/test/src/checkpoints.test.ts +0 -88
  66. package/test/src/chunked_snapshots.test.ts +0 -160
  67. package/test/src/env.ts +0 -11
  68. package/test/src/large_batch.test.ts +0 -239
  69. package/test/src/pg_test.test.ts +0 -729
  70. package/test/src/resuming_snapshots.test.ts +0 -163
  71. package/test/src/route_api_adapter.test.ts +0 -62
  72. package/test/src/schema_changes.test.ts +0 -675
  73. package/test/src/setup.ts +0 -12
  74. package/test/src/slow_tests.test.ts +0 -412
  75. package/test/src/storage_combination.test.ts +0 -35
  76. package/test/src/types/registry.test.ts +0 -149
  77. package/test/src/util.ts +0 -145
  78. package/test/src/validation.test.ts +0 -63
  79. package/test/src/wal_stream.test.ts +0 -618
  80. package/test/src/wal_stream_utils.ts +0 -292
  81. package/test/tsconfig.json +0 -27
  82. package/tsconfig.json +0 -34
  83. package/tsconfig.tsbuildinfo +0 -1
  84. package/vitest.config.ts +0 -3
@@ -1,675 +0,0 @@
1
- import { compareIds, putOp, reduceBucket, removeOp, test_utils } from '@powersync/service-core-tests';
2
- import * as timers from 'timers/promises';
3
- import { describe, expect, test } from 'vitest';
4
-
5
- import { describeWithStorage, StorageVersionTestContext } from './util.js';
6
- import { WalStreamTestContext } from './wal_stream_utils.js';
7
-
8
- describe('schema changes', { timeout: 20_000 }, function () {
9
- describeWithStorage({}, defineTests);
10
- });
11
-
12
- const BASIC_SYNC_RULES = `
13
- bucket_definitions:
14
- global:
15
- data:
16
- - SELECT id, * FROM "test_data"
17
- `;
18
-
19
- const PUT_T1 = test_utils.putOp('test_data', { id: 't1', description: 'test1' });
20
- const PUT_T2 = test_utils.putOp('test_data', { id: 't2', description: 'test2' });
21
- const PUT_T3 = test_utils.putOp('test_data', { id: 't3', description: 'test3' });
22
-
23
- const REMOVE_T1 = test_utils.removeOp('test_data', 't1');
24
- const REMOVE_T2 = test_utils.removeOp('test_data', 't2');
25
-
26
- function defineTests({ factory, storageVersion }: StorageVersionTestContext) {
27
- const openContext = (options?: Parameters<typeof WalStreamTestContext.open>[1]) => {
28
- return WalStreamTestContext.open(factory, { ...options, storageVersion });
29
- };
30
- test('re-create table', async () => {
31
- await using context = await openContext();
32
-
33
- // Drop a table and re-create it.
34
- await context.updateSyncRules(BASIC_SYNC_RULES);
35
- const { pool } = context;
36
-
37
- await pool.query(`DROP TABLE IF EXISTS test_data`);
38
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
39
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
40
-
41
- await context.replicateSnapshot();
42
- context.startStreaming();
43
-
44
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
45
-
46
- await pool.query(
47
- { statement: `DROP TABLE test_data` },
48
- { statement: `CREATE TABLE test_data(id text primary key, description text)` },
49
- { statement: `INSERT INTO test_data(id, description) VALUES('t3', 'test3')` }
50
- );
51
-
52
- const data = await context.getBucketData('global[]');
53
-
54
- // "Reduce" the bucket to get a stable output to test.
55
- // slice(1) to skip the CLEAR op.
56
- const reduced = reduceBucket(data).slice(1);
57
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T3]);
58
-
59
- // Initial inserts
60
- expect(data.slice(0, 2)).toMatchObject([PUT_T1, PUT_T2]);
61
-
62
- // Truncate - order doesn't matter
63
- expect(data.slice(2, 4).sort(compareIds)).toMatchObject([REMOVE_T1, REMOVE_T2]);
64
-
65
- expect(data.slice(4)).toMatchObject([
66
- // Snapshot insert
67
- PUT_T3,
68
- // Replicated insert
69
- // We may eventually be able to de-duplicate this
70
- PUT_T3
71
- ]);
72
- });
73
-
74
- test('add table', async () => {
75
- await using context = await openContext();
76
- // Add table after initial replication
77
- await context.updateSyncRules(BASIC_SYNC_RULES);
78
- const { pool } = context;
79
-
80
- await context.replicateSnapshot();
81
- context.startStreaming();
82
-
83
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
84
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
85
-
86
- const data = await context.getBucketData('global[]');
87
-
88
- // "Reduce" the bucket to get a stable output to test.
89
- // slice(1) to skip the CLEAR op.
90
- const reduced = reduceBucket(data).slice(1);
91
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T1]);
92
-
93
- expect(data).toMatchObject([
94
- // Snapshot insert
95
- PUT_T1,
96
- // Replicated insert
97
- // We may eventually be able to de-duplicate this
98
- PUT_T1
99
- ]);
100
- });
101
-
102
- test('rename table (1)', async () => {
103
- await using context = await openContext();
104
- const { pool } = context;
105
-
106
- await context.updateSyncRules(BASIC_SYNC_RULES);
107
-
108
- // Rename table not in sync rules -> in sync rules
109
- await pool.query(`CREATE TABLE test_data_old(id text primary key, description text)`);
110
- await pool.query(`INSERT INTO test_data_old(id, description) VALUES('t1', 'test1')`);
111
-
112
- await context.replicateSnapshot();
113
- context.startStreaming();
114
-
115
- await pool.query(
116
- { statement: `ALTER TABLE test_data_old RENAME TO test_data` },
117
- // We need an operation to detect the change
118
- { statement: `INSERT INTO test_data(id, description) VALUES('t2', 'test2')` }
119
- );
120
-
121
- const data = await context.getBucketData('global[]');
122
-
123
- // "Reduce" the bucket to get a stable output to test.
124
- // slice(1) to skip the CLEAR op.
125
- const reduced = reduceBucket(data).slice(1);
126
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
127
-
128
- expect(data.slice(0, 2).sort(compareIds)).toMatchObject([
129
- // Snapshot insert
130
- PUT_T1,
131
- PUT_T2
132
- ]);
133
- expect(data.slice(2)).toMatchObject([
134
- // Replicated insert
135
- // We may eventually be able to de-duplicate this
136
- PUT_T2
137
- ]);
138
- });
139
-
140
- test('rename table (2)', async () => {
141
- await using context = await openContext();
142
- // Rename table in sync rules -> in sync rules
143
- const { pool } = context;
144
-
145
- await context.updateSyncRules(`
146
- bucket_definitions:
147
- global:
148
- data:
149
- - SELECT id, * FROM "test_data%"
150
- `);
151
-
152
- await pool.query(`CREATE TABLE test_data1(id text primary key, description text)`);
153
- await pool.query(`INSERT INTO test_data1(id, description) VALUES('t1', 'test1')`);
154
-
155
- await context.replicateSnapshot();
156
- context.startStreaming();
157
-
158
- await pool.query(
159
- { statement: `ALTER TABLE test_data1 RENAME TO test_data2` },
160
- // We need an operation to detect the change
161
- { statement: `INSERT INTO test_data2(id, description) VALUES('t2', 'test2')` }
162
- );
163
-
164
- const data = await context.getBucketData('global[]');
165
-
166
- // "Reduce" the bucket to get a stable output to test.
167
- // slice(1) to skip the CLEAR op.
168
- const reduced = reduceBucket(data).slice(1);
169
- expect(reduced.sort(compareIds)).toMatchObject([
170
- putOp('test_data2', { id: 't1', description: 'test1' }),
171
- putOp('test_data2', { id: 't2', description: 'test2' })
172
- ]);
173
-
174
- expect(data.slice(0, 2)).toMatchObject([
175
- // Initial replication
176
- putOp('test_data1', { id: 't1', description: 'test1' }),
177
- // Initial truncate
178
- removeOp('test_data1', 't1')
179
- ]);
180
-
181
- expect(data.slice(2, 4).sort(compareIds)).toMatchObject([
182
- // Snapshot insert
183
- putOp('test_data2', { id: 't1', description: 'test1' }),
184
- putOp('test_data2', { id: 't2', description: 'test2' })
185
- ]);
186
- expect(data.slice(4)).toMatchObject([
187
- // Replicated insert
188
- // We may eventually be able to de-duplicate this
189
- putOp('test_data2', { id: 't2', description: 'test2' })
190
- ]);
191
- });
192
-
193
- test('rename table (3)', async () => {
194
- await using context = await openContext();
195
- // Rename table in sync rules -> not in sync rules
196
-
197
- const { pool } = context;
198
-
199
- await context.updateSyncRules(BASIC_SYNC_RULES);
200
-
201
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
202
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
203
-
204
- await context.replicateSnapshot();
205
- context.startStreaming();
206
-
207
- await pool.query(
208
- { statement: `ALTER TABLE test_data RENAME TO test_data_na` },
209
- // We need an operation to detect the change
210
- { statement: `INSERT INTO test_data_na(id, description) VALUES('t2', 'test2')` }
211
- );
212
-
213
- const data = await context.getBucketData('global[]');
214
-
215
- expect(data).toMatchObject([
216
- // Initial replication
217
- PUT_T1,
218
- // Truncate
219
- REMOVE_T1
220
- ]);
221
-
222
- // "Reduce" the bucket to get a stable output to test.
223
- // slice(1) to skip the CLEAR op.
224
- const reduced = reduceBucket(data).slice(1);
225
- expect(reduced.sort(compareIds)).toMatchObject([]);
226
- });
227
-
228
- test('change replica id', async () => {
229
- await using context = await openContext();
230
- // Change replica id from default to full
231
- // Causes a re-import of the table.
232
-
233
- const { pool } = context;
234
- await context.updateSyncRules(BASIC_SYNC_RULES);
235
-
236
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
237
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
238
-
239
- await context.replicateSnapshot();
240
- context.startStreaming();
241
-
242
- await pool.query(
243
- { statement: `ALTER TABLE test_data REPLICA IDENTITY FULL` },
244
- // We need an operation to detect the change
245
- { statement: `INSERT INTO test_data(id, description) VALUES('t2', 'test2')` }
246
- );
247
-
248
- const data = await context.getBucketData('global[]');
249
-
250
- // "Reduce" the bucket to get a stable output to test.
251
- // slice(1) to skip the CLEAR op.
252
- const reduced = reduceBucket(data).slice(1);
253
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
254
-
255
- expect(data.slice(0, 2)).toMatchObject([
256
- // Initial inserts
257
- PUT_T1,
258
- // Truncate
259
- REMOVE_T1
260
- ]);
261
-
262
- // Snapshot - order doesn't matter
263
- expect(data.slice(2, 4).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
264
-
265
- expect(data.slice(4).sort(compareIds)).toMatchObject([
266
- // Replicated insert
267
- // We may eventually be able to de-duplicate this
268
- PUT_T2
269
- ]);
270
- });
271
-
272
- test('change full replica id by adding column', async () => {
273
- await using context = await openContext();
274
- // Change replica id from full by adding column
275
- // Causes a re-import of the table.
276
- // Other changes such as renaming column would have the same effect
277
-
278
- const { pool } = context;
279
- await context.updateSyncRules(BASIC_SYNC_RULES);
280
-
281
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
282
- await pool.query(`ALTER TABLE test_data REPLICA IDENTITY FULL`);
283
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
284
-
285
- await context.replicateSnapshot();
286
- context.startStreaming();
287
-
288
- await pool.query(
289
- { statement: `ALTER TABLE test_data ADD COLUMN other TEXT` },
290
- { statement: `INSERT INTO test_data(id, description) VALUES('t2', 'test2')` }
291
- );
292
-
293
- const data = await context.getBucketData('global[]');
294
-
295
- expect(data.slice(0, 2)).toMatchObject([
296
- // Initial inserts
297
- PUT_T1,
298
- // Truncate
299
- REMOVE_T1
300
- ]);
301
-
302
- // Snapshot - order doesn't matter
303
- expect(data.slice(2, 4).sort(compareIds)).toMatchObject([
304
- putOp('test_data', { id: 't1', description: 'test1', other: null }),
305
- putOp('test_data', { id: 't2', description: 'test2', other: null })
306
- ]);
307
-
308
- expect(data.slice(4).sort(compareIds)).toMatchObject([
309
- // Replicated insert
310
- // We may eventually be able to de-duplicate this
311
- putOp('test_data', { id: 't2', description: 'test2', other: null })
312
- ]);
313
- });
314
-
315
- test('change default replica id by changing column type', async () => {
316
- await using context = await openContext();
317
- // Change default replica id by changing column type
318
- // Causes a re-import of the table.
319
- const { pool } = context;
320
- await context.updateSyncRules(BASIC_SYNC_RULES);
321
-
322
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
323
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
324
-
325
- await context.replicateSnapshot();
326
- context.startStreaming();
327
-
328
- await pool.query(
329
- { statement: `ALTER TABLE test_data ALTER COLUMN id TYPE varchar` },
330
- { statement: `INSERT INTO test_data(id, description) VALUES('t2', 'test2')` }
331
- );
332
-
333
- const data = await context.getBucketData('global[]');
334
-
335
- expect(data.slice(0, 2)).toMatchObject([
336
- // Initial inserts
337
- PUT_T1,
338
- // Truncate
339
- REMOVE_T1
340
- ]);
341
-
342
- // Snapshot - order doesn't matter
343
- expect(data.slice(2, 4).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2]);
344
-
345
- expect(data.slice(4).sort(compareIds)).toMatchObject([
346
- // Replicated insert
347
- // We may eventually be able to de-duplicate this
348
- PUT_T2
349
- ]);
350
- });
351
-
352
- test('change index id by changing column type', async () => {
353
- await using context = await openContext();
354
- // Change index replica id by changing column type
355
- // Causes a re-import of the table.
356
- // Secondary functionality tested here is that replica id column order stays
357
- // the same between initial and incremental replication.
358
- const { pool } = context;
359
- await context.updateSyncRules(BASIC_SYNC_RULES);
360
-
361
- await pool.query(`CREATE TABLE test_data(id text primary key, description text not null)`);
362
- await pool.query(`CREATE UNIQUE INDEX i1 ON test_data(description, id)`);
363
- await pool.query(`ALTER TABLE test_data REPLICA IDENTITY USING INDEX i1`);
364
-
365
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
366
-
367
- await context.replicateSnapshot();
368
- context.startStreaming();
369
-
370
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
371
-
372
- await pool.query(
373
- { statement: `ALTER TABLE test_data ALTER COLUMN description TYPE varchar` },
374
- { statement: `INSERT INTO test_data(id, description) VALUES('t3', 'test3')` }
375
- );
376
-
377
- const data = await context.getBucketData('global[]');
378
-
379
- expect(data.slice(0, 2)).toMatchObject([
380
- // Initial snapshot
381
- PUT_T1,
382
- // Streamed
383
- PUT_T2
384
- ]);
385
-
386
- // "Reduce" the bucket to get a stable output to test.
387
- // slice(1) to skip the CLEAR op.
388
- const reduced = reduceBucket(data).slice(1);
389
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2, PUT_T3]);
390
-
391
- // Previously had more specific tests, but this varies too much based on timing:
392
- // expect(data.slice(2, 4).sort(compareIds)).toMatchObject([
393
- // // Truncate - any order
394
- // REMOVE_T1,
395
- // REMOVE_T2
396
- // ]);
397
-
398
- // // Snapshot - order doesn't matter
399
- // expect(data.slice(4, 7).sort(compareIds)).toMatchObject([PUT_T1, PUT_T2, PUT_T3]);
400
-
401
- // expect(data.slice(7).sort(compareIds)).toMatchObject([
402
- // // Replicated insert
403
- // // We may eventually be able to de-duplicate this
404
- // PUT_T3
405
- // ]);
406
- });
407
-
408
- test('add to publication', async () => {
409
- await using context = await openContext();
410
- // Add table to publication after initial replication
411
- const { pool } = context;
412
-
413
- await pool.query(`DROP PUBLICATION powersync`);
414
- await pool.query(`CREATE TABLE test_foo(id text primary key, description text)`);
415
- await pool.query(`CREATE PUBLICATION powersync FOR table test_foo`);
416
-
417
- const storage = await context.updateSyncRules(BASIC_SYNC_RULES);
418
-
419
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
420
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
421
-
422
- await context.replicateSnapshot();
423
- context.startStreaming();
424
-
425
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
426
-
427
- await pool.query(`ALTER PUBLICATION powersync ADD TABLE test_data`);
428
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t3', 'test3')`);
429
-
430
- const data = await context.getBucketData('global[]');
431
-
432
- expect(data.slice(0, 3).sort(compareIds)).toMatchObject([
433
- // Snapshot insert - any order
434
- PUT_T1,
435
- PUT_T2,
436
- PUT_T3
437
- ]);
438
-
439
- expect(data.slice(3)).toMatchObject([
440
- // Replicated insert
441
- // We may eventually be able to de-duplicate this
442
- PUT_T3
443
- ]);
444
-
445
- // "Reduce" the bucket to get a stable output to test.
446
- // slice(1) to skip the CLEAR op.
447
- const reduced = reduceBucket(data).slice(1);
448
- expect(reduced.sort(compareIds)).toMatchObject([PUT_T1, PUT_T2, PUT_T3]);
449
- });
450
-
451
- test('add to publication (not in sync rules)', async () => {
452
- await using context = await openContext();
453
- // Add table to publication after initial replication
454
- // Since the table is not in sync rules, it should not be replicated.
455
- const { pool } = context;
456
-
457
- await pool.query(`DROP PUBLICATION powersync`);
458
- await pool.query(`CREATE TABLE test_foo(id text primary key, description text)`);
459
- await pool.query(`CREATE PUBLICATION powersync FOR table test_foo`);
460
-
461
- const storage = await context.updateSyncRules(BASIC_SYNC_RULES);
462
-
463
- await pool.query(`CREATE TABLE test_other(id text primary key, description text)`);
464
- await pool.query(`INSERT INTO test_other(id, description) VALUES('t1', 'test1')`);
465
-
466
- await context.replicateSnapshot();
467
- context.startStreaming();
468
-
469
- await pool.query(`INSERT INTO test_other(id, description) VALUES('t2', 'test2')`);
470
-
471
- await pool.query(`ALTER PUBLICATION powersync ADD TABLE test_other`);
472
- await pool.query(`INSERT INTO test_other(id, description) VALUES('t3', 'test3')`);
473
-
474
- const data = await context.getBucketData('global[]');
475
- expect(data).toMatchObject([]);
476
- });
477
-
478
- test('replica identity nothing', async () => {
479
- await using context = await openContext();
480
- // Technically not a schema change, but fits here.
481
- // Replica ID works a little differently here - the table doesn't have
482
- // one defined, but we generate a unique one for each replicated row.
483
-
484
- const { pool } = context;
485
- await context.updateSyncRules(BASIC_SYNC_RULES);
486
-
487
- await pool.query(`CREATE TABLE test_data(id text primary key, description text)`);
488
- await pool.query(`ALTER TABLE test_data REPLICA IDENTITY NOTHING`);
489
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
490
-
491
- await context.replicateSnapshot();
492
- context.startStreaming();
493
-
494
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
495
-
496
- // Just as an FYI - cannot update or delete here
497
- await expect(pool.query(`UPDATE test_data SET description = 'test2b' WHERE id = 't2'`)).rejects.toThrow(
498
- 'does not have a replica identity and publishes updates'
499
- );
500
-
501
- // Testing TRUNCATE is important here - this depends on current_data having unique
502
- // ids.
503
- await pool.query(`TRUNCATE TABLE test_data`);
504
-
505
- const data = await context.getBucketData('global[]');
506
-
507
- expect(data.slice(0, 2)).toMatchObject([
508
- // Initial inserts
509
- PUT_T1,
510
- PUT_T2
511
- ]);
512
-
513
- expect(data.slice(2).sort(compareIds)).toMatchObject([
514
- // Truncate
515
- REMOVE_T1,
516
- REMOVE_T2
517
- ]);
518
-
519
- // "Reduce" the bucket to get a stable output to test.
520
- // slice(1) to skip the CLEAR op.
521
- const reduced = reduceBucket(data).slice(1);
522
- expect(reduced.sort(compareIds)).toMatchObject([]);
523
- });
524
-
525
- test('replica identity default without PK', async () => {
526
- await using context = await openContext();
527
- // Same as no replica identity
528
- const { pool } = context;
529
- await context.updateSyncRules(BASIC_SYNC_RULES);
530
-
531
- await pool.query(`CREATE TABLE test_data(id text, description text)`);
532
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t1', 'test1')`);
533
-
534
- await context.replicateSnapshot();
535
- context.startStreaming();
536
-
537
- await pool.query(`INSERT INTO test_data(id, description) VALUES('t2', 'test2')`);
538
-
539
- // Just as an FYI - cannot update or delete here
540
- await expect(pool.query(`UPDATE test_data SET description = 'test2b' WHERE id = 't2'`)).rejects.toThrow(
541
- 'does not have a replica identity and publishes updates'
542
- );
543
-
544
- // Testing TRUNCATE is important here - this depends on current_data having unique
545
- // ids.
546
- await pool.query(`TRUNCATE TABLE test_data`);
547
-
548
- const data = await context.getBucketData('global[]');
549
-
550
- expect(data.slice(0, 2)).toMatchObject([
551
- // Initial inserts
552
- PUT_T1,
553
- PUT_T2
554
- ]);
555
-
556
- expect(data.slice(2).sort(compareIds)).toMatchObject([
557
- // Truncate
558
- REMOVE_T1,
559
- REMOVE_T2
560
- ]);
561
-
562
- // "Reduce" the bucket to get a stable output to test.
563
- // slice(1) to skip the CLEAR op.
564
- const reduced = reduceBucket(data).slice(1);
565
- expect(reduced.sort(compareIds)).toMatchObject([]);
566
- });
567
-
568
- // Test consistency of table snapshots.
569
- // Renames a table to trigger a snapshot.
570
- // To trigger the failure, modify the snapshot implementation to
571
- // introduce an arbitrary delay (in WalStream.ts):
572
- //
573
- // const rs = await db.query(`select pg_current_wal_lsn() as lsn`);
574
- // lsn = rs.rows[0][0];
575
- // await new Promise((resolve) => setTimeout(resolve, 100));
576
- // await this.snapshotTable(batch, db, result.table);
577
- test('table snapshot consistency', async () => {
578
- await using context = await openContext();
579
- const { pool } = context;
580
-
581
- await context.updateSyncRules(BASIC_SYNC_RULES);
582
-
583
- // Rename table not in sync rules -> in sync rules
584
- await pool.query(`CREATE TABLE test_data_old(id text primary key, num integer)`);
585
- await pool.query(`INSERT INTO test_data_old(id, num) VALUES('t1', 0)`);
586
- await pool.query(`INSERT INTO test_data_old(id, num) VALUES('t2', 0)`);
587
-
588
- await context.replicateSnapshot();
589
- context.startStreaming();
590
-
591
- await pool.query(
592
- { statement: `ALTER TABLE test_data_old RENAME TO test_data` },
593
- // This first update will trigger a snapshot
594
- { statement: `UPDATE test_data SET num = 0 WHERE id = 't2'` }
595
- );
596
-
597
- // Need some delay for the snapshot to be triggered
598
- await timers.setTimeout(5);
599
-
600
- let stop = false;
601
-
602
- let failures: any[] = [];
603
-
604
- // This is a tight loop that checks that t2.num >= t1.num
605
- const p = (async () => {
606
- let lopid = '';
607
- while (!stop) {
608
- const data = await context.getCurrentBucketData('global[]');
609
- const last = data[data.length - 1];
610
- if (last == null) {
611
- continue;
612
- }
613
- if (last.op_id != lopid) {
614
- const reduced = reduceBucket(data);
615
- reduced.shift();
616
- lopid = last.op_id;
617
-
618
- const t1 = reduced.find((op) => op.object_id == 't1');
619
- const t2 = reduced.find((op) => op.object_id == 't2');
620
- if (t1 && t2) {
621
- const d1 = JSON.parse(t1.data as string);
622
- const d2 = JSON.parse(t2.data as string);
623
- if (d1.num > d2.num) {
624
- failures.push({ d1, d2 });
625
- }
626
- }
627
- }
628
- }
629
- })();
630
-
631
- // We always have t2.num >= t1.num
632
- for (let i = 1; i <= 20; i++) {
633
- await pool.query({ statement: `UPDATE test_data SET num = ${i} WHERE id = 't2'` });
634
- }
635
- await pool.query({ statement: `UPDATE test_data SET num = 20 WHERE id = 't1'` });
636
-
637
- await context.getBucketData('global[]');
638
- stop = true;
639
- await p;
640
-
641
- expect(failures).toEqual([]);
642
- });
643
-
644
- test('custom types', async () => {
645
- await using context = await openContext();
646
-
647
- await context.updateSyncRules(`
648
- streams:
649
- stream:
650
- query: SELECT * FROM "test_data"
651
-
652
- config:
653
- edition: 2
654
- `);
655
-
656
- const { pool } = context;
657
- await pool.query(`CREATE TABLE test_data(id text primary key);`);
658
- await pool.query(`INSERT INTO test_data(id) VALUES ('t1')`);
659
-
660
- await context.replicateSnapshot();
661
- context.startStreaming();
662
-
663
- await pool.query(
664
- { statement: `CREATE TYPE composite AS (foo bool, bar int4);` },
665
- { statement: `ALTER TABLE test_data ADD COLUMN other composite;` },
666
- { statement: `UPDATE test_data SET other = ROW(TRUE, 2)::composite;` }
667
- );
668
-
669
- const data = await context.getBucketData('1#stream|0[]');
670
- expect(data).toMatchObject([
671
- putOp('test_data', { id: 't1' }),
672
- putOp('test_data', { id: 't1', other: '{"foo":1,"bar":2}' })
673
- ]);
674
- });
675
- }
package/test/src/setup.ts DELETED
@@ -1,12 +0,0 @@
1
- import { container } from '@powersync/lib-services-framework';
2
- import { METRICS_HELPER } from '@powersync/service-core-tests';
3
- import { beforeAll, beforeEach } from 'vitest';
4
-
5
- beforeAll(async () => {
6
- // Executes for every test file
7
- container.registerDefaults();
8
- });
9
-
10
- beforeEach(async () => {
11
- METRICS_HELPER.resetMetrics();
12
- });