@powersync/service-module-postgres 0.19.2 → 0.19.4

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