@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.
- package/dist/api/PostgresRouteAPIAdapter.d.ts +1 -1
- package/dist/api/PostgresRouteAPIAdapter.js +63 -72
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/MissingReplicationSlotError.d.ts +41 -0
- package/dist/replication/MissingReplicationSlotError.js +33 -0
- package/dist/replication/MissingReplicationSlotError.js.map +1 -0
- package/dist/replication/PostgresErrorRateLimiter.js +1 -1
- package/dist/replication/PostgresErrorRateLimiter.js.map +1 -1
- package/dist/replication/SnapshotQuery.js +2 -2
- package/dist/replication/SnapshotQuery.js.map +1 -1
- package/dist/replication/WalStream.d.ts +37 -14
- package/dist/replication/WalStream.js +145 -41
- package/dist/replication/WalStream.js.map +1 -1
- package/dist/replication/WalStreamReplicationJob.d.ts +1 -1
- package/dist/replication/WalStreamReplicationJob.js +7 -4
- package/dist/replication/WalStreamReplicationJob.js.map +1 -1
- package/dist/replication/WalStreamReplicator.d.ts +0 -1
- package/dist/replication/WalStreamReplicator.js +0 -22
- package/dist/replication/WalStreamReplicator.js.map +1 -1
- package/dist/replication/replication-index.d.ts +3 -1
- package/dist/replication/replication-index.js +3 -1
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/replication/replication-utils.d.ts +3 -11
- package/dist/replication/replication-utils.js +101 -164
- package/dist/replication/replication-utils.js.map +1 -1
- package/dist/replication/wal-budget-utils.d.ts +23 -0
- package/dist/replication/wal-budget-utils.js +57 -0
- package/dist/replication/wal-budget-utils.js.map +1 -0
- package/dist/types/registry.js +1 -1
- package/dist/types/registry.js.map +1 -1
- package/package.json +15 -11
- package/sql/check-source-configuration.plpgsql +13 -0
- package/sql/debug-tables-info-batched.plpgsql +230 -0
- package/CHANGELOG.md +0 -843
- package/src/api/PostgresRouteAPIAdapter.ts +0 -356
- package/src/index.ts +0 -1
- package/src/module/PostgresModule.ts +0 -122
- package/src/replication/ConnectionManagerFactory.ts +0 -33
- package/src/replication/PgManager.ts +0 -122
- package/src/replication/PgRelation.ts +0 -41
- package/src/replication/PostgresErrorRateLimiter.ts +0 -48
- package/src/replication/SnapshotQuery.ts +0 -213
- package/src/replication/WalStream.ts +0 -1157
- package/src/replication/WalStreamReplicationJob.ts +0 -138
- package/src/replication/WalStreamReplicator.ts +0 -79
- package/src/replication/replication-index.ts +0 -5
- package/src/replication/replication-utils.ts +0 -398
- package/src/types/registry.ts +0 -275
- package/src/types/resolver.ts +0 -227
- package/src/types/types.ts +0 -44
- package/src/utils/application-name.ts +0 -8
- package/src/utils/migration_lib.ts +0 -80
- package/src/utils/populate_test_data.ts +0 -37
- package/src/utils/populate_test_data_worker.ts +0 -53
- package/src/utils/postgres_version.ts +0 -8
- package/test/src/checkpoints.test.ts +0 -86
- package/test/src/chunked_snapshots.test.ts +0 -161
- package/test/src/env.ts +0 -11
- package/test/src/large_batch.test.ts +0 -241
- package/test/src/pg_test.test.ts +0 -729
- package/test/src/resuming_snapshots.test.ts +0 -160
- package/test/src/route_api_adapter.test.ts +0 -62
- package/test/src/schema_changes.test.ts +0 -655
- package/test/src/setup.ts +0 -12
- package/test/src/slow_tests.test.ts +0 -519
- package/test/src/storage_combination.test.ts +0 -35
- package/test/src/types/registry.test.ts +0 -149
- package/test/src/util.ts +0 -151
- package/test/src/validation.test.ts +0 -63
- package/test/src/wal_stream.test.ts +0 -607
- package/test/src/wal_stream_utils.ts +0 -284
- package/test/tsconfig.json +0 -27
- package/tsconfig.json +0 -34
- package/tsconfig.tsbuildinfo +0 -1
- 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
|
-
});
|