@powersync/service-module-postgres 0.0.0-dev-20240918092408

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 (87) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +67 -0
  3. package/README.md +3 -0
  4. package/dist/api/PostgresRouteAPIAdapter.d.ts +22 -0
  5. package/dist/api/PostgresRouteAPIAdapter.js +273 -0
  6. package/dist/api/PostgresRouteAPIAdapter.js.map +1 -0
  7. package/dist/auth/SupabaseKeyCollector.d.ts +22 -0
  8. package/dist/auth/SupabaseKeyCollector.js +64 -0
  9. package/dist/auth/SupabaseKeyCollector.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/module/PostgresModule.d.ts +14 -0
  14. package/dist/module/PostgresModule.js +108 -0
  15. package/dist/module/PostgresModule.js.map +1 -0
  16. package/dist/replication/ConnectionManagerFactory.d.ts +10 -0
  17. package/dist/replication/ConnectionManagerFactory.js +21 -0
  18. package/dist/replication/ConnectionManagerFactory.js.map +1 -0
  19. package/dist/replication/PgManager.d.ts +25 -0
  20. package/dist/replication/PgManager.js +60 -0
  21. package/dist/replication/PgManager.js.map +1 -0
  22. package/dist/replication/PgRelation.d.ts +6 -0
  23. package/dist/replication/PgRelation.js +27 -0
  24. package/dist/replication/PgRelation.js.map +1 -0
  25. package/dist/replication/PostgresErrorRateLimiter.d.ts +11 -0
  26. package/dist/replication/PostgresErrorRateLimiter.js +43 -0
  27. package/dist/replication/PostgresErrorRateLimiter.js.map +1 -0
  28. package/dist/replication/WalStream.d.ts +53 -0
  29. package/dist/replication/WalStream.js +536 -0
  30. package/dist/replication/WalStream.js.map +1 -0
  31. package/dist/replication/WalStreamReplicationJob.d.ts +27 -0
  32. package/dist/replication/WalStreamReplicationJob.js +131 -0
  33. package/dist/replication/WalStreamReplicationJob.js.map +1 -0
  34. package/dist/replication/WalStreamReplicator.d.ts +13 -0
  35. package/dist/replication/WalStreamReplicator.js +36 -0
  36. package/dist/replication/WalStreamReplicator.js.map +1 -0
  37. package/dist/replication/replication-index.d.ts +5 -0
  38. package/dist/replication/replication-index.js +6 -0
  39. package/dist/replication/replication-index.js.map +1 -0
  40. package/dist/replication/replication-utils.d.ts +32 -0
  41. package/dist/replication/replication-utils.js +272 -0
  42. package/dist/replication/replication-utils.js.map +1 -0
  43. package/dist/types/types.d.ts +76 -0
  44. package/dist/types/types.js +110 -0
  45. package/dist/types/types.js.map +1 -0
  46. package/dist/utils/migration_lib.d.ts +11 -0
  47. package/dist/utils/migration_lib.js +64 -0
  48. package/dist/utils/migration_lib.js.map +1 -0
  49. package/dist/utils/pgwire_utils.d.ts +16 -0
  50. package/dist/utils/pgwire_utils.js +70 -0
  51. package/dist/utils/pgwire_utils.js.map +1 -0
  52. package/dist/utils/populate_test_data.d.ts +8 -0
  53. package/dist/utils/populate_test_data.js +65 -0
  54. package/dist/utils/populate_test_data.js.map +1 -0
  55. package/package.json +49 -0
  56. package/src/api/PostgresRouteAPIAdapter.ts +307 -0
  57. package/src/auth/SupabaseKeyCollector.ts +70 -0
  58. package/src/index.ts +5 -0
  59. package/src/module/PostgresModule.ts +122 -0
  60. package/src/replication/ConnectionManagerFactory.ts +28 -0
  61. package/src/replication/PgManager.ts +70 -0
  62. package/src/replication/PgRelation.ts +31 -0
  63. package/src/replication/PostgresErrorRateLimiter.ts +44 -0
  64. package/src/replication/WalStream.ts +639 -0
  65. package/src/replication/WalStreamReplicationJob.ts +142 -0
  66. package/src/replication/WalStreamReplicator.ts +45 -0
  67. package/src/replication/replication-index.ts +5 -0
  68. package/src/replication/replication-utils.ts +329 -0
  69. package/src/types/types.ts +159 -0
  70. package/src/utils/migration_lib.ts +79 -0
  71. package/src/utils/pgwire_utils.ts +73 -0
  72. package/src/utils/populate_test_data.ts +77 -0
  73. package/test/src/__snapshots__/pg_test.test.ts.snap +256 -0
  74. package/test/src/env.ts +7 -0
  75. package/test/src/large_batch.test.ts +195 -0
  76. package/test/src/pg_test.test.ts +450 -0
  77. package/test/src/schema_changes.test.ts +543 -0
  78. package/test/src/setup.ts +7 -0
  79. package/test/src/slow_tests.test.ts +335 -0
  80. package/test/src/util.ts +105 -0
  81. package/test/src/validation.test.ts +64 -0
  82. package/test/src/wal_stream.test.ts +319 -0
  83. package/test/src/wal_stream_utils.ts +121 -0
  84. package/test/tsconfig.json +28 -0
  85. package/tsconfig.json +31 -0
  86. package/tsconfig.tsbuildinfo +1 -0
  87. package/vitest.config.ts +9 -0
@@ -0,0 +1,450 @@
1
+ import { constructAfterRecord } from '@module/utils/pgwire_utils.js';
2
+ import * as pgwire from '@powersync/service-jpgwire';
3
+ import { SqliteRow } from '@powersync/service-sync-rules';
4
+ import { describe, expect, test } from 'vitest';
5
+ import { clearTestDb, connectPgPool, connectPgWire, TEST_URI } from './util.js';
6
+ import { WalStream } from '@module/replication/WalStream.js';
7
+
8
+ describe('pg data types', () => {
9
+ async function setupTable(db: pgwire.PgClient) {
10
+ await clearTestDb(db);
11
+ await db.query(`CREATE TABLE test_data(
12
+ id serial primary key,
13
+ text text,
14
+ uuid uuid,
15
+ varchar varchar(255),
16
+ bool bool,
17
+ bytea bytea,
18
+ int2 int2,
19
+ int4 int4,
20
+ int8 int8,
21
+ float4 float4,
22
+ float8 float8,
23
+ numeric numeric, -- same as decimal
24
+ json json,
25
+ jsonb jsonb,
26
+ pg_lsn pg_lsn,
27
+ date date,
28
+ time time,
29
+ timestamp timestamp,
30
+ timestamptz timestamptz,
31
+ interval interval,
32
+ macaddr macaddr,
33
+ inet inet,
34
+ oid oid
35
+ )`);
36
+
37
+ await db.query(`DROP TABLE IF EXISTS test_data_arrays`);
38
+ await db.query(`CREATE TABLE test_data_arrays(
39
+ id serial primary key,
40
+ text text[],
41
+ uuid uuid[],
42
+ varchar varchar(255)[],
43
+ bool bool[],
44
+ bytea bytea[],
45
+ int2 int2[],
46
+ int4 int4[],
47
+ int8 int8[],
48
+ float4 float4[],
49
+ float8 float8[],
50
+ numeric numeric[], -- same as decimal
51
+ json json[],
52
+ jsonb jsonb[],
53
+ pg_lsn pg_lsn[],
54
+ date date[],
55
+ time time[],
56
+ timestamp timestamp[],
57
+ timestamptz timestamptz[],
58
+ interval interval[],
59
+ macaddr macaddr[],
60
+ inet inet[],
61
+ oid oid[],
62
+ multidimensional text[][] -- same as text[]
63
+ )`);
64
+ }
65
+
66
+ async function insert(db: pgwire.PgClient) {
67
+ await db.query(`
68
+ INSERT INTO test_data(id, text, uuid, varchar, bool, bytea, int2, int4, int8, numeric, float4, float8)
69
+ VALUES(1, 'text', 'baeb2514-4c57-436d-b3cc-c1256211656d', 'varchar', true, 'test', 1000, 1000000, 9007199254740993, 18014398509481982123, 3.14, 314);
70
+
71
+ INSERT INTO test_data(id, json, jsonb)
72
+ VALUES(2, '{"test": "thing" }', '{"test": "thing" }');
73
+
74
+ INSERT INTO test_data(id, date, time, timestamp, timestamptz)
75
+ VALUES(3, '2023-03-06', '15:47', '2023-03-06 15:47', '2023-03-06 15:47+02');
76
+
77
+ INSERT INTO test_data(id, pg_lsn, interval, macaddr, inet, oid)
78
+ VALUES(4, '016/B374D848', '1 hour', '00:00:5e:00:53:af', '127.0.0.1', 1007);
79
+
80
+ INSERT INTO test_data(id, date, time, timestamp, timestamptz)
81
+ VALUES(5, '-infinity'::date, 'allballs'::time, '-infinity'::timestamp, '-infinity'::timestamptz);
82
+
83
+ INSERT INTO test_data(id, timestamp, timestamptz)
84
+ VALUES(6, 'epoch'::timestamp, 'epoch'::timestamptz);
85
+
86
+ INSERT INTO test_data(id, timestamp, timestamptz)
87
+ VALUES(7, 'infinity'::timestamp, 'infinity'::timestamptz);
88
+
89
+ INSERT INTO test_data(id, timestamptz)
90
+ VALUES(8, '0022-02-03 12:13:14+03'::timestamptz);
91
+
92
+ INSERT INTO test_data(id, timestamptz)
93
+ VALUES(9, '10022-02-03 12:13:14+03'::timestamptz);
94
+ `);
95
+ }
96
+
97
+ async function insertArrays(db: pgwire.PgClient) {
98
+ await db.query(`
99
+ INSERT INTO test_data_arrays(id, text, uuid, varchar, bool, bytea, int2, int4, int8, numeric)
100
+ VALUES(1, ARRAY['text', '}te][xt{"'], '{"baeb2514-4c57-436d-b3cc-c1256211656d"}', '{"varchar"}', '{true}', '{"test"}', '{1000}', '{1000000}', '{9007199254740993}', '{18014398509481982123}');
101
+
102
+ INSERT INTO test_data_arrays(id, json, jsonb)
103
+ VALUES(2, ARRAY['{"test": "thing"}' :: json, '{"test": "}te][xt{"}' :: json], ARRAY['{"test": "thing", "foo": 5.0, "bignum": 18014398509481982123, "bool":true}' :: jsonb]);
104
+
105
+ INSERT INTO test_data_arrays(id, date, time, timestamp, timestamptz)
106
+ VALUES(3, ARRAY['2023-03-06'::date], ARRAY['15:47'::time], ARRAY['2023-03-06 15:47'::timestamp], ARRAY['2023-03-06 15:47+02'::timestamptz, '2023-03-06 15:47:00.123450+02'::timestamptz]);
107
+
108
+ INSERT INTO test_data_arrays(id, pg_lsn, interval, macaddr, inet, oid)
109
+ VALUES(4, ARRAY['016/B374D848'::pg_lsn], ARRAY['1 hour'::interval], ARRAY['00:00:5e:00:53:af'::macaddr], ARRAY['127.0.0.1'::inet], ARRAY[1007::oid]);
110
+
111
+ -- Empty arrays
112
+ INSERT INTO test_data_arrays(id, text, uuid, varchar, bool, bytea, int2, int4, int8, numeric)
113
+ VALUES(5, ARRAY[]::text[], ARRAY[]::uuid[], ARRAY[]::varchar[], ARRAY[]::bool[], ARRAY[]::bytea[], ARRAY[]::int2[], ARRAY[]::int4[], ARRAY[]::int8[], ARRAY[]::numeric[]);
114
+
115
+ -- Two-dimentional array
116
+ INSERT INTO test_data_arrays(id, multidimensional)
117
+ VALUES(6, ARRAY[['one', 1], ['two', 2], ['three', Null]]::TEXT[]);
118
+
119
+ -- Empty array
120
+ INSERT INTO test_data_arrays(id, multidimensional)
121
+ VALUES(7, ARRAY[[], [], []]::TEXT[]);
122
+
123
+ -- Empty array
124
+ INSERT INTO test_data_arrays(id, multidimensional)
125
+ VALUES(8, ARRAY[]::TEXT[]);
126
+
127
+ -- Array with only null
128
+ INSERT INTO test_data_arrays(id, multidimensional)
129
+ VALUES(9, ARRAY[NULL]::TEXT[]);
130
+
131
+ -- Array with 'null'
132
+ INSERT INTO test_data_arrays(id, multidimensional)
133
+ VALUES(10, ARRAY['null']::TEXT[]);
134
+ `);
135
+ }
136
+
137
+ function checkResults(transformed: Record<string, any>[]) {
138
+ expect(transformed[0]).toMatchObject({
139
+ id: 1n,
140
+ text: 'text',
141
+ uuid: 'baeb2514-4c57-436d-b3cc-c1256211656d',
142
+ varchar: 'varchar',
143
+ bool: 1n,
144
+ bytea: new Uint8Array([116, 101, 115, 116]),
145
+ int2: 1000n,
146
+ int4: 1000000n,
147
+ int8: 9007199254740993n,
148
+ float4: 3.14,
149
+ float8: 314,
150
+ numeric: '18014398509481982123'
151
+ });
152
+ expect(transformed[1]).toMatchObject({
153
+ id: 2n,
154
+ json: '{"test": "thing" }', // Whitespace preserved
155
+ jsonb: '{"test": "thing"}' // Whitespace according to pg JSON conventions
156
+ });
157
+
158
+ expect(transformed[2]).toMatchObject({
159
+ id: 3n,
160
+ date: '2023-03-06',
161
+ time: '15:47:00',
162
+ timestamp: '2023-03-06 15:47:00',
163
+ timestamptz: '2023-03-06 13:47:00Z'
164
+ });
165
+
166
+ expect(transformed[3]).toMatchObject({
167
+ id: 4n,
168
+ pg_lsn: '00000016/B374D848',
169
+ interval: '01:00:00',
170
+ macaddr: '00:00:5e:00:53:af',
171
+ inet: '127.0.0.1',
172
+ oid: 1007n
173
+ });
174
+
175
+ expect(transformed[4]).toMatchObject({
176
+ id: 5n,
177
+ date: '0000-01-01',
178
+ time: '00:00:00',
179
+ timestamp: '0000-01-01 00:00:00',
180
+ timestamptz: '0000-01-01 00:00:00Z'
181
+ });
182
+
183
+ expect(transformed[5]).toMatchObject({
184
+ id: 6n,
185
+ timestamp: '1970-01-01 00:00:00',
186
+ timestamptz: '1970-01-01 00:00:00Z'
187
+ });
188
+
189
+ expect(transformed[6]).toMatchObject({
190
+ id: 7n,
191
+ timestamp: '9999-12-31 23:59:59',
192
+ timestamptz: '9999-12-31 23:59:59Z'
193
+ });
194
+
195
+ expect(transformed[7]).toMatchObject({
196
+ id: 8n,
197
+ timestamptz: '0022-02-03 09:13:14Z'
198
+ });
199
+
200
+ expect(transformed[8]).toMatchObject({
201
+ id: 9n,
202
+ // 10022-02-03 12:13:14+03 - out of range of both our date parsing logic, and sqlite's date functions
203
+ // We can consider just preserving the source string as an alternative if this causes issues.
204
+ timestamptz: null
205
+ });
206
+ }
207
+
208
+ function checkResultArrays(transformed: Record<string, any>[]) {
209
+ expect(transformed[0]).toMatchObject({
210
+ id: 1n,
211
+ text: `["text","}te][xt{\\""]`,
212
+ uuid: '["baeb2514-4c57-436d-b3cc-c1256211656d"]',
213
+ varchar: '["varchar"]',
214
+ bool: '[1]',
215
+ bytea: '[null]',
216
+ int2: '[1000]',
217
+ int4: '[1000000]',
218
+ int8: `[9007199254740993]`,
219
+ numeric: `["18014398509481982123"]`
220
+ });
221
+
222
+ // Note: Depending on to what extent we use the original postgres value, the whitespace may change, and order may change.
223
+ // We do expect that decimals and big numbers are preserved.
224
+ expect(transformed[1]).toMatchObject({
225
+ id: 2n,
226
+ // Expected output after a serialize + parse cycle:
227
+ // json: `[{"test":"thing"},{"test":"}te][xt{"}]`,
228
+ // jsonb: `[{"foo":5.0,"bool":true,"test":"thing","bignum":18014398509481982123}]`
229
+ // Expected using direct PG values:
230
+ json: `[{"test": "thing"},{"test": "}te][xt{"}]`,
231
+ jsonb: `[{"foo": 5.0, "bool": true, "test": "thing", "bignum": 18014398509481982123}]`
232
+ });
233
+
234
+ expect(transformed[2]).toMatchObject({
235
+ id: 3n,
236
+ date: `["2023-03-06"]`,
237
+ time: `["15:47:00"]`,
238
+ timestamp: `["2023-03-06 15:47:00"]`,
239
+ timestamptz: `["2023-03-06 13:47:00Z","2023-03-06 13:47:00.12345Z"]`
240
+ });
241
+
242
+ expect(transformed[3]).toMatchObject({
243
+ id: 4n,
244
+ pg_lsn: `["00000016/B374D848"]`,
245
+ interval: `["01:00:00"]`,
246
+ macaddr: `["00:00:5e:00:53:af"]`,
247
+ inet: `["127.0.0.1"]`,
248
+ oid: `[1007]`
249
+ });
250
+
251
+ expect(transformed[4]).toMatchObject({
252
+ id: 5n,
253
+ text: '[]',
254
+ uuid: '[]',
255
+ varchar: '[]',
256
+ bool: '[]',
257
+ bytea: '[]',
258
+ int2: '[]',
259
+ int4: '[]',
260
+ int8: '[]',
261
+ numeric: '[]'
262
+ });
263
+
264
+ expect(transformed[5]).toMatchObject({
265
+ id: 6n,
266
+ multidimensional: '[["one","1"],["two","2"],["three",null]]'
267
+ });
268
+
269
+ expect(transformed[6]).toMatchObject({
270
+ id: 7n,
271
+ multidimensional: '[]'
272
+ });
273
+
274
+ expect(transformed[7]).toMatchObject({
275
+ id: 8n,
276
+ multidimensional: '[]'
277
+ });
278
+
279
+ expect(transformed[8]).toMatchObject({
280
+ id: 9n,
281
+ multidimensional: '[null]'
282
+ });
283
+
284
+ expect(transformed[9]).toMatchObject({
285
+ id: 10n,
286
+ multidimensional: '["null"]'
287
+ });
288
+ }
289
+
290
+ test('test direct queries', async () => {
291
+ const db = await connectPgPool();
292
+ try {
293
+ await setupTable(db);
294
+
295
+ await insert(db);
296
+
297
+ const transformed = [
298
+ ...WalStream.getQueryData(pgwire.pgwireRows(await db.query(`SELECT * FROM test_data ORDER BY id`)))
299
+ ];
300
+
301
+ checkResults(transformed);
302
+ } finally {
303
+ await db.end();
304
+ }
305
+ });
306
+
307
+ test('test direct queries - parameterized', async () => {
308
+ // Parameterized queries may use a different underlying protocol,
309
+ // so we make sure it also gets the same results.
310
+ const db = await connectPgPool();
311
+ try {
312
+ await setupTable(db);
313
+
314
+ await insert(db);
315
+
316
+ const transformed = [
317
+ ...WalStream.getQueryData(
318
+ pgwire.pgwireRows(
319
+ await db.query({
320
+ statement: `SELECT * FROM test_data WHERE $1 ORDER BY id`,
321
+ params: [{ type: 'bool', value: true }]
322
+ })
323
+ )
324
+ )
325
+ ];
326
+
327
+ checkResults(transformed);
328
+ } finally {
329
+ await db.end();
330
+ }
331
+ });
332
+
333
+ test('test direct queries - arrays', async () => {
334
+ const db = await connectPgPool();
335
+ try {
336
+ await setupTable(db);
337
+
338
+ await insertArrays(db);
339
+
340
+ const transformed = [
341
+ ...WalStream.getQueryData(pgwire.pgwireRows(await db.query(`SELECT * FROM test_data_arrays ORDER BY id`)))
342
+ ];
343
+
344
+ checkResultArrays(transformed);
345
+ } finally {
346
+ await db.end();
347
+ }
348
+ });
349
+
350
+ test('test replication', async () => {
351
+ const db = await connectPgPool();
352
+ try {
353
+ await setupTable(db);
354
+
355
+ const slotName = 'test_slot';
356
+
357
+ await db.query({
358
+ statement: 'SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_name = $1',
359
+ params: [{ type: 'varchar', value: slotName }]
360
+ });
361
+
362
+ await db.query({
363
+ statement: `SELECT slot_name, lsn FROM pg_catalog.pg_create_logical_replication_slot($1, 'pgoutput')`,
364
+ params: [{ type: 'varchar', value: slotName }]
365
+ });
366
+
367
+ await insert(db);
368
+
369
+ const pg: pgwire.PgConnection = await pgwire.pgconnect({ replication: 'database' }, TEST_URI);
370
+ const replicationStream = await pg.logicalReplication({
371
+ slot: slotName,
372
+ options: {
373
+ proto_version: '1',
374
+ publication_names: 'powersync'
375
+ }
376
+ });
377
+
378
+ const transformed = await getReplicationTx(replicationStream);
379
+ await pg.end();
380
+
381
+ checkResults(transformed);
382
+ } finally {
383
+ await db.end();
384
+ }
385
+ });
386
+
387
+ test('test replication - arrays', async () => {
388
+ const db = await connectPgPool();
389
+ try {
390
+ await setupTable(db);
391
+
392
+ const slotName = 'test_slot';
393
+
394
+ await db.query({
395
+ statement: 'SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_name = $1',
396
+ params: [{ type: 'varchar', value: slotName }]
397
+ });
398
+
399
+ await db.query({
400
+ statement: `SELECT slot_name, lsn FROM pg_catalog.pg_create_logical_replication_slot($1, 'pgoutput')`,
401
+ params: [{ type: 'varchar', value: slotName }]
402
+ });
403
+
404
+ await insertArrays(db);
405
+
406
+ const pg: pgwire.PgConnection = await pgwire.pgconnect({ replication: 'database' }, TEST_URI);
407
+ const replicationStream = await pg.logicalReplication({
408
+ slot: slotName,
409
+ options: {
410
+ proto_version: '1',
411
+ publication_names: 'powersync'
412
+ }
413
+ });
414
+
415
+ const transformed = await getReplicationTx(replicationStream);
416
+ await pg.end();
417
+
418
+ checkResultArrays(transformed);
419
+ } finally {
420
+ await db.end();
421
+ }
422
+ });
423
+
424
+ test('schema', async function () {
425
+ const db = await connectPgWire();
426
+
427
+ await setupTable(db);
428
+
429
+ // TODO need a test for adapter
430
+ // const schema = await api.getConnectionsSchema(db);
431
+ // expect(schema).toMatchSnapshot();
432
+ });
433
+ });
434
+
435
+ /**
436
+ * Return all the inserts from the first transaction in the replication stream.
437
+ */
438
+ async function getReplicationTx(replicationStream: pgwire.ReplicationStream) {
439
+ let transformed: SqliteRow[] = [];
440
+ for await (const batch of replicationStream.pgoutputDecode()) {
441
+ for (const msg of batch.messages) {
442
+ if (msg.tag == 'insert') {
443
+ transformed.push(constructAfterRecord(msg));
444
+ } else if (msg.tag == 'commit') {
445
+ return transformed;
446
+ }
447
+ }
448
+ }
449
+ return transformed;
450
+ }