@powersync/service-module-postgres-storage 0.14.0 → 0.15.0
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/CHANGELOG.md +33 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +1 -2
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +11 -2
- package/dist/storage/PostgresSyncRulesStorage.js +8 -159
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.js +263 -13
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/PostgresSyncRulesStorage.ts +12 -171
- package/src/storage/batch/PostgresBucketBatch.ts +306 -24
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
23
23
|
import * as sync_rules from '@powersync/service-sync-rules';
|
|
24
24
|
import * as timers from 'timers/promises';
|
|
25
|
-
import * as uuid from 'uuid';
|
|
26
25
|
import { bigint, BIGINT_MAX } from '../types/codecs.js';
|
|
27
26
|
import { models, RequiredOperationBatchLimits } from '../types/types.js';
|
|
28
27
|
import { replicaIdToSubkey } from '../utils/bson.js';
|
|
@@ -32,7 +31,6 @@ import * as framework from '@powersync/lib-services-framework';
|
|
|
32
31
|
import { StatementParam } from '@powersync/service-jpgwire';
|
|
33
32
|
import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
|
|
34
33
|
import * as t from 'ts-codec';
|
|
35
|
-
import { SourceTableDecoded, StoredRelationId } from '../types/models/SourceTable.js';
|
|
36
34
|
import { pick } from '../utils/ts-codec.js';
|
|
37
35
|
import { PostgresBucketBatch } from './batch/PostgresBucketBatch.js';
|
|
38
36
|
import { PostgresWriteCheckpointAPI } from './checkpoints/PostgresWriteCheckpointAPI.js';
|
|
@@ -69,7 +67,7 @@ export class PostgresSyncRulesStorage
|
|
|
69
67
|
|
|
70
68
|
// TODO we might be able to share this in an abstract class
|
|
71
69
|
private parsedSyncRulesCache:
|
|
72
|
-
| { parsed: sync_rules.
|
|
70
|
+
| { parsed: sync_rules.HydratedSyncConfig; options: storage.ParseSyncRulesOptions }
|
|
73
71
|
| undefined;
|
|
74
72
|
private _checksumCache: storage.ChecksumCache | undefined;
|
|
75
73
|
|
|
@@ -109,14 +107,14 @@ export class PostgresSyncRulesStorage
|
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
// TODO we might be able to share this in an abstract class
|
|
112
|
-
getParsedSyncRules(options: storage.ParseSyncRulesOptions): sync_rules.
|
|
110
|
+
getParsedSyncRules(options: storage.ParseSyncRulesOptions): sync_rules.HydratedSyncConfig {
|
|
113
111
|
const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
|
|
114
112
|
/**
|
|
115
113
|
* Check if the cached sync config, if present, had the same options.
|
|
116
114
|
* Parse sync config if the options are different or if there is no cached value.
|
|
117
115
|
*/
|
|
118
116
|
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
119
|
-
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).
|
|
117
|
+
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncConfig(), options };
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
return this.parsedSyncRulesCache!.parsed;
|
|
@@ -188,168 +186,6 @@ export class PostgresSyncRulesStorage
|
|
|
188
186
|
);
|
|
189
187
|
}
|
|
190
188
|
|
|
191
|
-
async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
|
|
192
|
-
const { group_id, connection_id, connection_tag, entity_descriptor } = options;
|
|
193
|
-
|
|
194
|
-
const { schema, name: table, objectId, replicaIdColumns } = entity_descriptor;
|
|
195
|
-
|
|
196
|
-
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
197
|
-
name: column.name,
|
|
198
|
-
type: column.type,
|
|
199
|
-
// The PGWire returns this as a BigInt. We want to store this as JSONB
|
|
200
|
-
type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
|
|
201
|
-
}));
|
|
202
|
-
return this.db.transaction(async (db) => {
|
|
203
|
-
let sourceTableRow: SourceTableDecoded | null;
|
|
204
|
-
if (objectId != null) {
|
|
205
|
-
sourceTableRow = await db.sql`
|
|
206
|
-
SELECT
|
|
207
|
-
*
|
|
208
|
-
FROM
|
|
209
|
-
source_tables
|
|
210
|
-
WHERE
|
|
211
|
-
group_id = ${{ type: 'int4', value: group_id }}
|
|
212
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
213
|
-
AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
214
|
-
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
215
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
216
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
217
|
-
`
|
|
218
|
-
.decoded(models.SourceTable)
|
|
219
|
-
.first();
|
|
220
|
-
} else {
|
|
221
|
-
sourceTableRow = await db.sql`
|
|
222
|
-
SELECT
|
|
223
|
-
*
|
|
224
|
-
FROM
|
|
225
|
-
source_tables
|
|
226
|
-
WHERE
|
|
227
|
-
group_id = ${{ type: 'int4', value: group_id }}
|
|
228
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
229
|
-
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
230
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
231
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
232
|
-
`
|
|
233
|
-
.decoded(models.SourceTable)
|
|
234
|
-
.first();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (sourceTableRow == null) {
|
|
238
|
-
const row = await db.sql`
|
|
239
|
-
INSERT INTO
|
|
240
|
-
source_tables (
|
|
241
|
-
id,
|
|
242
|
-
group_id,
|
|
243
|
-
connection_id,
|
|
244
|
-
relation_id,
|
|
245
|
-
schema_name,
|
|
246
|
-
table_name,
|
|
247
|
-
replica_id_columns
|
|
248
|
-
)
|
|
249
|
-
VALUES
|
|
250
|
-
(
|
|
251
|
-
${{ type: 'varchar', value: uuid.v4() }},
|
|
252
|
-
${{ type: 'int4', value: group_id }},
|
|
253
|
-
${{ type: 'int4', value: connection_id }},
|
|
254
|
-
--- The objectId can be string | number | undefined, we store it as jsonb value
|
|
255
|
-
${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
|
|
256
|
-
${{ type: 'varchar', value: schema }},
|
|
257
|
-
${{ type: 'varchar', value: table }},
|
|
258
|
-
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
259
|
-
)
|
|
260
|
-
RETURNING
|
|
261
|
-
*
|
|
262
|
-
`
|
|
263
|
-
.decoded(models.SourceTable)
|
|
264
|
-
.first();
|
|
265
|
-
sourceTableRow = row;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const sourceTable = new storage.SourceTable({
|
|
269
|
-
id: sourceTableRow!.id,
|
|
270
|
-
connectionTag: connection_tag,
|
|
271
|
-
objectId: objectId,
|
|
272
|
-
schema: schema,
|
|
273
|
-
name: table,
|
|
274
|
-
replicaIdColumns: replicaIdColumns,
|
|
275
|
-
snapshotComplete: sourceTableRow!.snapshot_done ?? true
|
|
276
|
-
});
|
|
277
|
-
if (!sourceTable.snapshotComplete) {
|
|
278
|
-
sourceTable.snapshotStatus = {
|
|
279
|
-
totalEstimatedCount: Number(sourceTableRow!.snapshot_total_estimated_count ?? -1n),
|
|
280
|
-
replicatedCount: Number(sourceTableRow!.snapshot_replicated_count ?? 0n),
|
|
281
|
-
lastKey: sourceTableRow!.snapshot_last_key
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
sourceTable.syncEvent = options.sync_rules.tableTriggersEvent(sourceTable);
|
|
285
|
-
sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
|
|
286
|
-
sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
|
|
287
|
-
|
|
288
|
-
let truncatedTables: SourceTableDecoded[] = [];
|
|
289
|
-
if (objectId != null) {
|
|
290
|
-
// relation_id present - check for renamed tables
|
|
291
|
-
truncatedTables = await db.sql`
|
|
292
|
-
SELECT
|
|
293
|
-
*
|
|
294
|
-
FROM
|
|
295
|
-
source_tables
|
|
296
|
-
WHERE
|
|
297
|
-
group_id = ${{ type: 'int4', value: group_id }}
|
|
298
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
299
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
300
|
-
AND (
|
|
301
|
-
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
302
|
-
OR (
|
|
303
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
304
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
305
|
-
)
|
|
306
|
-
)
|
|
307
|
-
`
|
|
308
|
-
.decoded(models.SourceTable)
|
|
309
|
-
.rows();
|
|
310
|
-
} else {
|
|
311
|
-
// relation_id not present - only check for changed replica_id_columns
|
|
312
|
-
truncatedTables = await db.sql`
|
|
313
|
-
SELECT
|
|
314
|
-
*
|
|
315
|
-
FROM
|
|
316
|
-
source_tables
|
|
317
|
-
WHERE
|
|
318
|
-
group_id = ${{ type: 'int4', value: group_id }}
|
|
319
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
320
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
321
|
-
AND (
|
|
322
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
323
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
324
|
-
)
|
|
325
|
-
`
|
|
326
|
-
.decoded(models.SourceTable)
|
|
327
|
-
.rows();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return {
|
|
331
|
-
table: sourceTable,
|
|
332
|
-
dropTables: truncatedTables.map(
|
|
333
|
-
(doc) =>
|
|
334
|
-
new storage.SourceTable({
|
|
335
|
-
id: doc.id,
|
|
336
|
-
connectionTag: connection_tag,
|
|
337
|
-
objectId: doc.relation_id?.object_id ?? 0,
|
|
338
|
-
schema: doc.schema_name,
|
|
339
|
-
name: doc.table_name,
|
|
340
|
-
replicaIdColumns:
|
|
341
|
-
doc.replica_id_columns?.map((c) => ({
|
|
342
|
-
name: c.name,
|
|
343
|
-
typeOid: c.typeId,
|
|
344
|
-
type: c.type
|
|
345
|
-
})) ?? [],
|
|
346
|
-
snapshotComplete: doc.snapshot_done ?? true
|
|
347
|
-
})
|
|
348
|
-
)
|
|
349
|
-
};
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
189
|
async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
|
|
354
190
|
const syncRules = await this.db.sql`
|
|
355
191
|
SELECT
|
|
@@ -370,7 +206,7 @@ export class PostgresSyncRulesStorage
|
|
|
370
206
|
const writer = new PostgresBucketBatch({
|
|
371
207
|
logger: options.logger ?? this.logger,
|
|
372
208
|
db: this.db,
|
|
373
|
-
sync_rules: this.sync_rules.parsed(options).
|
|
209
|
+
sync_rules: this.sync_rules.parsed(options).hydratedSyncConfig(),
|
|
374
210
|
group_id: this.group_id,
|
|
375
211
|
slot_name: this.slot_name,
|
|
376
212
|
last_checkpoint_lsn: checkpoint_lsn,
|
|
@@ -380,6 +216,7 @@ export class PostgresSyncRulesStorage
|
|
|
380
216
|
skip_existing_rows: options.skipExistingRows ?? false,
|
|
381
217
|
batch_limits: this.options.batchLimits,
|
|
382
218
|
markRecordUnavailable: options.markRecordUnavailable,
|
|
219
|
+
hooks: options.hooks,
|
|
383
220
|
storageConfig: this.storageConfig
|
|
384
221
|
});
|
|
385
222
|
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
@@ -670,13 +507,16 @@ export class PostgresSyncRulesStorage
|
|
|
670
507
|
snapshot_done,
|
|
671
508
|
snapshot_lsn,
|
|
672
509
|
last_checkpoint_lsn,
|
|
673
|
-
state
|
|
510
|
+
state,
|
|
511
|
+
keepalive_op
|
|
674
512
|
FROM
|
|
675
513
|
sync_rules
|
|
676
514
|
WHERE
|
|
677
515
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
678
516
|
`
|
|
679
|
-
.decoded(
|
|
517
|
+
.decoded(
|
|
518
|
+
pick(models.SyncRules, ['snapshot_done', 'last_checkpoint_lsn', 'state', 'snapshot_lsn', 'keepalive_op'])
|
|
519
|
+
)
|
|
680
520
|
.first();
|
|
681
521
|
|
|
682
522
|
if (syncRulesRow == null) {
|
|
@@ -687,7 +527,8 @@ export class PostgresSyncRulesStorage
|
|
|
687
527
|
snapshot_done: syncRulesRow.snapshot_done,
|
|
688
528
|
active: syncRulesRow.state == storage.SyncRuleState.ACTIVE,
|
|
689
529
|
checkpoint_lsn: syncRulesRow.last_checkpoint_lsn ?? null,
|
|
690
|
-
snapshot_lsn: syncRulesRow.snapshot_lsn ?? null
|
|
530
|
+
snapshot_lsn: syncRulesRow.snapshot_lsn ?? null,
|
|
531
|
+
keepalive_op: syncRulesRow.keepalive_op ?? null
|
|
691
532
|
};
|
|
692
533
|
}
|
|
693
534
|
|