@powersync/service-module-postgres-storage 0.0.0-dev-20250116115804

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 (157) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +67 -0
  3. package/README.md +67 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/@types/index.d.ts +7 -0
  6. package/dist/@types/migrations/PostgresMigrationAgent.d.ts +12 -0
  7. package/dist/@types/migrations/PostgresMigrationStore.d.ts +14 -0
  8. package/dist/@types/migrations/migration-utils.d.ts +3 -0
  9. package/dist/@types/migrations/scripts/1684951997326-init.d.ts +3 -0
  10. package/dist/@types/module/PostgresStorageModule.d.ts +6 -0
  11. package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +42 -0
  12. package/dist/@types/storage/PostgresCompactor.d.ts +40 -0
  13. package/dist/@types/storage/PostgresStorageProvider.d.ts +5 -0
  14. package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +46 -0
  15. package/dist/@types/storage/PostgresTestStorageFactoryGenerator.d.ts +13 -0
  16. package/dist/@types/storage/batch/OperationBatch.d.ts +47 -0
  17. package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +90 -0
  18. package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +64 -0
  19. package/dist/@types/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +20 -0
  20. package/dist/@types/storage/storage-index.d.ts +5 -0
  21. package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +17 -0
  22. package/dist/@types/types/codecs.d.ts +61 -0
  23. package/dist/@types/types/models/ActiveCheckpoint.d.ts +12 -0
  24. package/dist/@types/types/models/ActiveCheckpointNotification.d.ts +19 -0
  25. package/dist/@types/types/models/BucketData.d.ts +22 -0
  26. package/dist/@types/types/models/BucketParameters.d.ts +11 -0
  27. package/dist/@types/types/models/CurrentData.d.ts +22 -0
  28. package/dist/@types/types/models/Instance.d.ts +6 -0
  29. package/dist/@types/types/models/Migration.d.ts +12 -0
  30. package/dist/@types/types/models/SourceTable.d.ts +31 -0
  31. package/dist/@types/types/models/SyncRules.d.ts +47 -0
  32. package/dist/@types/types/models/WriteCheckpoint.d.ts +15 -0
  33. package/dist/@types/types/models/models-index.d.ts +10 -0
  34. package/dist/@types/types/types.d.ts +96 -0
  35. package/dist/@types/utils/bson.d.ts +6 -0
  36. package/dist/@types/utils/bucket-data.d.ts +18 -0
  37. package/dist/@types/utils/db.d.ts +8 -0
  38. package/dist/@types/utils/ts-codec.d.ts +5 -0
  39. package/dist/@types/utils/utils-index.d.ts +4 -0
  40. package/dist/index.js +8 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/migrations/PostgresMigrationAgent.js +36 -0
  43. package/dist/migrations/PostgresMigrationAgent.js.map +1 -0
  44. package/dist/migrations/PostgresMigrationStore.js +60 -0
  45. package/dist/migrations/PostgresMigrationStore.js.map +1 -0
  46. package/dist/migrations/migration-utils.js +13 -0
  47. package/dist/migrations/migration-utils.js.map +1 -0
  48. package/dist/migrations/scripts/1684951997326-init.js +196 -0
  49. package/dist/migrations/scripts/1684951997326-init.js.map +1 -0
  50. package/dist/module/PostgresStorageModule.js +23 -0
  51. package/dist/module/PostgresStorageModule.js.map +1 -0
  52. package/dist/storage/PostgresBucketStorageFactory.js +433 -0
  53. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -0
  54. package/dist/storage/PostgresCompactor.js +298 -0
  55. package/dist/storage/PostgresCompactor.js.map +1 -0
  56. package/dist/storage/PostgresStorageProvider.js +35 -0
  57. package/dist/storage/PostgresStorageProvider.js.map +1 -0
  58. package/dist/storage/PostgresSyncRulesStorage.js +619 -0
  59. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -0
  60. package/dist/storage/PostgresTestStorageFactoryGenerator.js +110 -0
  61. package/dist/storage/PostgresTestStorageFactoryGenerator.js.map +1 -0
  62. package/dist/storage/batch/OperationBatch.js +93 -0
  63. package/dist/storage/batch/OperationBatch.js.map +1 -0
  64. package/dist/storage/batch/PostgresBucketBatch.js +732 -0
  65. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -0
  66. package/dist/storage/batch/PostgresPersistedBatch.js +367 -0
  67. package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -0
  68. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +148 -0
  69. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -0
  70. package/dist/storage/storage-index.js +6 -0
  71. package/dist/storage/storage-index.js.map +1 -0
  72. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +58 -0
  73. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -0
  74. package/dist/types/codecs.js +97 -0
  75. package/dist/types/codecs.js.map +1 -0
  76. package/dist/types/models/ActiveCheckpoint.js +12 -0
  77. package/dist/types/models/ActiveCheckpoint.js.map +1 -0
  78. package/dist/types/models/ActiveCheckpointNotification.js +8 -0
  79. package/dist/types/models/ActiveCheckpointNotification.js.map +1 -0
  80. package/dist/types/models/BucketData.js +23 -0
  81. package/dist/types/models/BucketData.js.map +1 -0
  82. package/dist/types/models/BucketParameters.js +11 -0
  83. package/dist/types/models/BucketParameters.js.map +1 -0
  84. package/dist/types/models/CurrentData.js +16 -0
  85. package/dist/types/models/CurrentData.js.map +1 -0
  86. package/dist/types/models/Instance.js +5 -0
  87. package/dist/types/models/Instance.js.map +1 -0
  88. package/dist/types/models/Migration.js +12 -0
  89. package/dist/types/models/Migration.js.map +1 -0
  90. package/dist/types/models/SourceTable.js +24 -0
  91. package/dist/types/models/SourceTable.js.map +1 -0
  92. package/dist/types/models/SyncRules.js +47 -0
  93. package/dist/types/models/SyncRules.js.map +1 -0
  94. package/dist/types/models/WriteCheckpoint.js +13 -0
  95. package/dist/types/models/WriteCheckpoint.js.map +1 -0
  96. package/dist/types/models/models-index.js +11 -0
  97. package/dist/types/models/models-index.js.map +1 -0
  98. package/dist/types/types.js +46 -0
  99. package/dist/types/types.js.map +1 -0
  100. package/dist/utils/bson.js +16 -0
  101. package/dist/utils/bson.js.map +1 -0
  102. package/dist/utils/bucket-data.js +25 -0
  103. package/dist/utils/bucket-data.js.map +1 -0
  104. package/dist/utils/db.js +24 -0
  105. package/dist/utils/db.js.map +1 -0
  106. package/dist/utils/ts-codec.js +11 -0
  107. package/dist/utils/ts-codec.js.map +1 -0
  108. package/dist/utils/utils-index.js +5 -0
  109. package/dist/utils/utils-index.js.map +1 -0
  110. package/package.json +50 -0
  111. package/src/index.ts +10 -0
  112. package/src/migrations/PostgresMigrationAgent.ts +46 -0
  113. package/src/migrations/PostgresMigrationStore.ts +70 -0
  114. package/src/migrations/migration-utils.ts +14 -0
  115. package/src/migrations/scripts/1684951997326-init.ts +141 -0
  116. package/src/module/PostgresStorageModule.ts +30 -0
  117. package/src/storage/PostgresBucketStorageFactory.ts +496 -0
  118. package/src/storage/PostgresCompactor.ts +366 -0
  119. package/src/storage/PostgresStorageProvider.ts +42 -0
  120. package/src/storage/PostgresSyncRulesStorage.ts +666 -0
  121. package/src/storage/PostgresTestStorageFactoryGenerator.ts +61 -0
  122. package/src/storage/batch/OperationBatch.ts +101 -0
  123. package/src/storage/batch/PostgresBucketBatch.ts +885 -0
  124. package/src/storage/batch/PostgresPersistedBatch.ts +441 -0
  125. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +176 -0
  126. package/src/storage/storage-index.ts +5 -0
  127. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +67 -0
  128. package/src/types/codecs.ts +136 -0
  129. package/src/types/models/ActiveCheckpoint.ts +15 -0
  130. package/src/types/models/ActiveCheckpointNotification.ts +14 -0
  131. package/src/types/models/BucketData.ts +26 -0
  132. package/src/types/models/BucketParameters.ts +14 -0
  133. package/src/types/models/CurrentData.ts +23 -0
  134. package/src/types/models/Instance.ts +8 -0
  135. package/src/types/models/Migration.ts +19 -0
  136. package/src/types/models/SourceTable.ts +32 -0
  137. package/src/types/models/SyncRules.ts +50 -0
  138. package/src/types/models/WriteCheckpoint.ts +20 -0
  139. package/src/types/models/models-index.ts +10 -0
  140. package/src/types/types.ts +73 -0
  141. package/src/utils/bson.ts +17 -0
  142. package/src/utils/bucket-data.ts +25 -0
  143. package/src/utils/db.ts +27 -0
  144. package/src/utils/ts-codec.ts +14 -0
  145. package/src/utils/utils-index.ts +4 -0
  146. package/test/src/__snapshots__/storage.test.ts.snap +9 -0
  147. package/test/src/__snapshots__/storage_sync.test.ts.snap +332 -0
  148. package/test/src/env.ts +6 -0
  149. package/test/src/migrations.test.ts +34 -0
  150. package/test/src/setup.ts +16 -0
  151. package/test/src/storage.test.ts +131 -0
  152. package/test/src/storage_compacting.test.ts +5 -0
  153. package/test/src/storage_sync.test.ts +12 -0
  154. package/test/src/util.ts +34 -0
  155. package/test/tsconfig.json +20 -0
  156. package/tsconfig.json +36 -0
  157. package/vitest.config.ts +13 -0
@@ -0,0 +1,366 @@
1
+ import * as lib_postgres from '@powersync/lib-service-postgres';
2
+ import { logger } from '@powersync/lib-services-framework';
3
+ import { storage, utils } from '@powersync/service-core';
4
+ import * as pgwire from '@powersync/service-jpgwire';
5
+ import * as t from 'ts-codec';
6
+ import { BIGINT_MAX } from '../types/codecs.js';
7
+ import { models } from '../types/types.js';
8
+ import { sql } from '../utils/db.js';
9
+ import { pick } from '../utils/ts-codec.js';
10
+ import { encodedCacheKey } from './batch/OperationBatch.js';
11
+
12
+ interface CurrentBucketState {
13
+ /** Bucket name */
14
+ bucket: string;
15
+ /**
16
+ * Rows seen in the bucket, with the last op_id of each.
17
+ */
18
+ seen: Map<string, bigint>;
19
+ /**
20
+ * Estimated memory usage of the seen Map.
21
+ */
22
+ trackingSize: number;
23
+
24
+ /**
25
+ * Last (lowest) seen op_id that is not a PUT.
26
+ */
27
+ lastNotPut: bigint | null;
28
+
29
+ /**
30
+ * Number of REMOVE/MOVE operations seen since lastNotPut.
31
+ */
32
+ opsSincePut: number;
33
+ }
34
+
35
+ /**
36
+ * Additional options, primarily for testing.
37
+ */
38
+ export interface PostgresCompactOptions extends storage.CompactOptions {
39
+ /** Minimum of 2 */
40
+ clearBatchLimit?: number;
41
+ /** Minimum of 1 */
42
+ moveBatchLimit?: number;
43
+ /** Minimum of 1 */
44
+ moveBatchQueryLimit?: number;
45
+ }
46
+
47
+ const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
48
+ const DEFAULT_MOVE_BATCH_LIMIT = 2000;
49
+ const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
50
+
51
+ /** This default is primarily for tests. */
52
+ const DEFAULT_MEMORY_LIMIT_MB = 64;
53
+
54
+ export class PostgresCompactor {
55
+ private updates: pgwire.Statement[] = [];
56
+
57
+ private idLimitBytes: number;
58
+ private moveBatchLimit: number;
59
+ private moveBatchQueryLimit: number;
60
+ private clearBatchLimit: number;
61
+ private maxOpId: bigint | undefined;
62
+ private buckets: string[] | undefined;
63
+
64
+ constructor(
65
+ private db: lib_postgres.DatabaseClient,
66
+ private group_id: number,
67
+ options?: PostgresCompactOptions
68
+ ) {
69
+ this.idLimitBytes = (options?.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
70
+ this.moveBatchLimit = options?.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
71
+ this.moveBatchQueryLimit = options?.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
72
+ this.clearBatchLimit = options?.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
73
+ this.maxOpId = options?.maxOpId;
74
+ this.buckets = options?.compactBuckets;
75
+ }
76
+
77
+ /**
78
+ * Compact buckets by converting operations into MOVE and/or CLEAR operations.
79
+ *
80
+ * See /docs/compacting-operations.md for details.
81
+ */
82
+ async compact() {
83
+ if (this.buckets) {
84
+ for (let bucket of this.buckets) {
85
+ // We can make this more efficient later on by iterating
86
+ // through the buckets in a single query.
87
+ // That makes batching more tricky, so we leave for later.
88
+ await this.compactInternal(bucket);
89
+ }
90
+ } else {
91
+ await this.compactInternal(undefined);
92
+ }
93
+ }
94
+
95
+ async compactInternal(bucket: string | undefined) {
96
+ const idLimitBytes = this.idLimitBytes;
97
+
98
+ let currentState: CurrentBucketState | null = null;
99
+
100
+ let bucketLower: string | null = null;
101
+ let bucketUpper: string | null = null;
102
+
103
+ if (bucket?.includes('[')) {
104
+ // Exact bucket name
105
+ bucketLower = bucket;
106
+ bucketUpper = bucket;
107
+ } else if (bucket) {
108
+ // Bucket definition name
109
+ bucketLower = `${bucket}[`;
110
+ bucketUpper = `${bucket}[\uFFFF`;
111
+ }
112
+
113
+ let upperOpIdLimit = BIGINT_MAX;
114
+
115
+ while (true) {
116
+ const batch = await this.db.sql`
117
+ SELECT
118
+ op,
119
+ op_id,
120
+ source_table,
121
+ table_name,
122
+ row_id,
123
+ source_key,
124
+ bucket_name
125
+ FROM
126
+ bucket_data
127
+ WHERE
128
+ group_id = ${{ type: 'int4', value: this.group_id }}
129
+ AND bucket_name LIKE COALESCE(${{ type: 'varchar', value: bucketLower }}, '%')
130
+ AND op_id < ${{ type: 'int8', value: upperOpIdLimit }}
131
+ ORDER BY
132
+ bucket_name,
133
+ op_id DESC
134
+ LIMIT
135
+ ${{ type: 'int4', value: this.moveBatchQueryLimit }}
136
+ `
137
+ .decoded(
138
+ pick(models.BucketData, ['op', 'source_table', 'table_name', 'source_key', 'row_id', 'op_id', 'bucket_name'])
139
+ )
140
+ .rows();
141
+
142
+ if (batch.length == 0) {
143
+ // We've reached the end
144
+ break;
145
+ }
146
+
147
+ // Set upperBound for the next batch
148
+ upperOpIdLimit = batch[batch.length - 1].op_id;
149
+
150
+ for (const doc of batch) {
151
+ if (currentState == null || doc.bucket_name != currentState.bucket) {
152
+ if (currentState != null && currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
153
+ // Important to flush before clearBucket()
154
+ await this.flush();
155
+ logger.info(
156
+ `Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
157
+ );
158
+
159
+ const bucket = currentState.bucket;
160
+ const clearOp = currentState.lastNotPut;
161
+ // Free memory before clearing bucket
162
+ currentState = null;
163
+ await this.clearBucket(bucket, clearOp);
164
+ }
165
+ currentState = {
166
+ bucket: doc.bucket_name,
167
+ seen: new Map(),
168
+ trackingSize: 0,
169
+ lastNotPut: null,
170
+ opsSincePut: 0
171
+ };
172
+ }
173
+
174
+ if (this.maxOpId != null && doc.op_id > this.maxOpId) {
175
+ continue;
176
+ }
177
+
178
+ let isPersistentPut = doc.op == 'PUT';
179
+
180
+ if (doc.op == 'REMOVE' || doc.op == 'PUT') {
181
+ const key = `${doc.table_name}/${doc.row_id}/${encodedCacheKey(doc.source_table!, doc.source_key!)}`;
182
+ const targetOp = currentState.seen.get(utils.flatstr(key));
183
+ if (targetOp) {
184
+ // Will convert to MOVE, so don't count as PUT
185
+ isPersistentPut = false;
186
+
187
+ this.updates.push(sql`
188
+ UPDATE bucket_data
189
+ SET
190
+ op = 'MOVE',
191
+ target_op = ${{ type: 'int8', value: targetOp }},
192
+ table_name = NULL,
193
+ row_id = NULL,
194
+ data = NULL,
195
+ source_table = NULL,
196
+ source_key = NULL
197
+ WHERE
198
+ group_id = ${{ type: 'int4', value: this.group_id }}
199
+ AND bucket_name = ${{ type: 'varchar', value: doc.bucket_name }}
200
+ AND op_id = ${{ type: 'int8', value: doc.op_id }}
201
+ `);
202
+ } else {
203
+ if (currentState.trackingSize >= idLimitBytes) {
204
+ // Reached memory limit.
205
+ // Keep the highest seen values in this case.
206
+ } else {
207
+ // flatstr reduces the memory usage by flattening the string
208
+ currentState.seen.set(utils.flatstr(key), doc.op_id);
209
+ // length + 16 for the string
210
+ // 24 for the bigint
211
+ // 50 for map overhead
212
+ // 50 for additional overhead
213
+ currentState.trackingSize += key.length + 140;
214
+ }
215
+ }
216
+ }
217
+
218
+ if (isPersistentPut) {
219
+ currentState.lastNotPut = null;
220
+ currentState.opsSincePut = 0;
221
+ } else if (doc.op != 'CLEAR') {
222
+ if (currentState.lastNotPut == null) {
223
+ currentState.lastNotPut = doc.op_id;
224
+ }
225
+ currentState.opsSincePut += 1;
226
+ }
227
+
228
+ if (this.updates.length >= this.moveBatchLimit) {
229
+ await this.flush();
230
+ }
231
+ }
232
+ }
233
+
234
+ await this.flush();
235
+ currentState?.seen.clear();
236
+ if (currentState?.lastNotPut != null && currentState?.opsSincePut > 1) {
237
+ logger.info(
238
+ `Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
239
+ );
240
+ const bucket = currentState.bucket;
241
+ const clearOp = currentState.lastNotPut;
242
+ // Free memory before clearing bucket
243
+ currentState = null;
244
+ await this.clearBucket(bucket, clearOp);
245
+ }
246
+ }
247
+
248
+ private async flush() {
249
+ if (this.updates.length > 0) {
250
+ logger.info(`Compacting ${this.updates.length} ops`);
251
+ await this.db.query(...this.updates);
252
+ this.updates = [];
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Perform a CLEAR compact for a bucket.
258
+ *
259
+ * @param bucket bucket name
260
+ * @param op op_id of the last non-PUT operation, which will be converted to CLEAR.
261
+ */
262
+ private async clearBucket(bucket: string, op: bigint) {
263
+ /**
264
+ * This entire method could be implemented as a Postgres function, but this might make debugging
265
+ * a bit more challenging.
266
+ */
267
+ let done = false;
268
+ while (!done) {
269
+ await this.db.lockConnection(async (db) => {
270
+ /**
271
+ * Start a transaction where each read returns the state at the start of the transaction,.
272
+ * Similar to the MongoDB readConcern: { level: 'snapshot' } mode.
273
+ */
274
+ await db.sql`BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ`.execute();
275
+
276
+ try {
277
+ let checksum = 0;
278
+ let lastOpId: bigint | null = null;
279
+ let targetOp: bigint | null = null;
280
+ let gotAnOp = false;
281
+
282
+ const codec = pick(models.BucketData, ['op', 'source_table', 'source_key', 'op_id', 'checksum', 'target_op']);
283
+ for await (const operations of db.streamRows<t.Encoded<typeof codec>>(sql`
284
+ SELECT
285
+ source_table,
286
+ source_key,
287
+ op,
288
+ op_id,
289
+ checksum,
290
+ target_op
291
+ FROM
292
+ bucket_data
293
+ WHERE
294
+ group_id = ${{ type: 'int4', value: this.group_id }}
295
+ AND bucket_name = ${{ type: 'varchar', value: bucket }}
296
+ AND op_id <= ${{ type: 'int8', value: op }}
297
+ ORDER BY
298
+ op_id
299
+ LIMIT
300
+ ${{ type: 'int4', value: this.clearBatchLimit }}
301
+ `)) {
302
+ const decodedOps = operations.map((o) => codec.decode(o));
303
+ for (const op of decodedOps) {
304
+ if ([models.OpType.MOVE, models.OpType.REMOVE, models.OpType.CLEAR].includes(op.op)) {
305
+ checksum = utils.addChecksums(checksum, Number(op.checksum));
306
+ lastOpId = op.op_id;
307
+ if (op.op != models.OpType.CLEAR) {
308
+ gotAnOp = true;
309
+ }
310
+ if (op.target_op != null) {
311
+ if (targetOp == null || op.target_op > targetOp) {
312
+ targetOp = op.target_op;
313
+ }
314
+ }
315
+ } else {
316
+ throw new Error(`Unexpected ${op.op} operation at ${this.group_id}:${bucket}:${op.op_id}`);
317
+ }
318
+ }
319
+ }
320
+
321
+ if (!gotAnOp) {
322
+ await db.sql`COMMIT`.execute();
323
+ done = true;
324
+ return;
325
+ }
326
+
327
+ logger.info(`Flushing CLEAR at ${lastOpId}`);
328
+
329
+ await db.sql`
330
+ DELETE FROM bucket_data
331
+ WHERE
332
+ group_id = ${{ type: 'int4', value: this.group_id }}
333
+ AND bucket_name = ${{ type: 'varchar', value: bucket }}
334
+ AND op_id <= ${{ type: 'int8', value: lastOpId }}
335
+ `.execute();
336
+
337
+ await db.sql`
338
+ INSERT INTO
339
+ bucket_data (
340
+ group_id,
341
+ bucket_name,
342
+ op_id,
343
+ op,
344
+ checksum,
345
+ target_op
346
+ )
347
+ VALUES
348
+ (
349
+ ${{ type: 'int4', value: this.group_id }},
350
+ ${{ type: 'varchar', value: bucket }},
351
+ ${{ type: 'int8', value: lastOpId }},
352
+ ${{ type: 'varchar', value: models.OpType.CLEAR }},
353
+ ${{ type: 'int8', value: checksum }},
354
+ ${{ type: 'int8', value: targetOp }}
355
+ )
356
+ `.execute();
357
+
358
+ await db.sql`COMMIT`.execute();
359
+ } catch (ex) {
360
+ await db.sql`ROLLBACK`.execute();
361
+ throw ex;
362
+ }
363
+ });
364
+ }
365
+ }
366
+ }
@@ -0,0 +1,42 @@
1
+ import * as lib_postgres from '@powersync/lib-service-postgres';
2
+ import { logger } from '@powersync/lib-services-framework';
3
+ import { storage } from '@powersync/service-core';
4
+
5
+ import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js';
6
+ import { dropTables } from '../utils/db.js';
7
+ import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
8
+
9
+ export class PostgresStorageProvider implements storage.BucketStorageProvider {
10
+ get type() {
11
+ return lib_postgres.POSTGRES_CONNECTION_TYPE;
12
+ }
13
+
14
+ async getStorage(options: storage.GetStorageOptions): Promise<storage.ActiveStorage> {
15
+ const { resolvedConfig } = options;
16
+
17
+ const { storage } = resolvedConfig;
18
+ if (!isPostgresStorageConfig(storage)) {
19
+ // This should not be reached since the generation should be managed externally.
20
+ throw new Error(
21
+ `Cannot create Postgres bucket storage with provided config ${storage.type} !== ${lib_postgres.POSTGRES_CONNECTION_TYPE}`
22
+ );
23
+ }
24
+
25
+ const decodedConfig = PostgresStorageConfig.decode(storage);
26
+ const normalizedConfig = normalizePostgresStorageConfig(decodedConfig);
27
+ const storageFactory = new PostgresBucketStorageFactory({
28
+ config: normalizedConfig,
29
+ slot_name_prefix: options.resolvedConfig.slot_name_prefix
30
+ });
31
+ return {
32
+ storage: storageFactory,
33
+ shutDown: async () => storageFactory.db[Symbol.asyncDispose](),
34
+ tearDown: async () => {
35
+ logger.info(`Tearing down Postgres storage: ${normalizedConfig.database}...`);
36
+ await dropTables(storageFactory.db);
37
+ await storageFactory.db[Symbol.asyncDispose]();
38
+ return true;
39
+ }
40
+ } satisfies storage.ActiveStorage;
41
+ }
42
+ }