@powersync/service-module-postgres-storage 0.8.4 → 0.9.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 +35 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +2 -3
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +10 -1
- package/dist/storage/PostgresSyncRulesStorage.js +48 -57
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.js +65 -2
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/PostgresSyncRulesStorage.ts +54 -76
- package/src/storage/batch/PostgresBucketBatch.ts +71 -10
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
InternalOpId,
|
|
8
8
|
internalToExternalOpId,
|
|
9
9
|
LastValueSink,
|
|
10
|
+
maxLsn,
|
|
11
|
+
ReplicationCheckpoint,
|
|
10
12
|
storage,
|
|
11
13
|
utils,
|
|
12
14
|
WatchWriteCheckpointOptions
|
|
@@ -135,18 +137,19 @@ export class PostgresSyncRulesStorage
|
|
|
135
137
|
.decoded(pick(models.SyncRules, ['last_checkpoint', 'last_checkpoint_lsn']))
|
|
136
138
|
.first();
|
|
137
139
|
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
return new PostgresReplicationCheckpoint(
|
|
141
|
+
this,
|
|
142
|
+
checkpointRow?.last_checkpoint ?? 0n,
|
|
143
|
+
checkpointRow?.last_checkpoint_lsn ?? null
|
|
144
|
+
);
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
async resolveTable(options: storage.ResolveTableOptions): Promise<storage.ResolveTableResult> {
|
|
145
148
|
const { group_id, connection_id, connection_tag, entity_descriptor } = options;
|
|
146
149
|
|
|
147
|
-
const { schema, name: table, objectId,
|
|
150
|
+
const { schema, name: table, objectId, replicaIdColumns } = entity_descriptor;
|
|
148
151
|
|
|
149
|
-
const
|
|
152
|
+
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
150
153
|
name: column.name,
|
|
151
154
|
type: column.type,
|
|
152
155
|
// The PGWire returns this as a BigInt. We want to store this as JSONB
|
|
@@ -166,7 +169,7 @@ export class PostgresSyncRulesStorage
|
|
|
166
169
|
AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
167
170
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
168
171
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
169
|
-
AND replica_id_columns = ${{ type: 'jsonb', value:
|
|
172
|
+
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
170
173
|
`
|
|
171
174
|
.decoded(models.SourceTable)
|
|
172
175
|
.first();
|
|
@@ -181,7 +184,7 @@ export class PostgresSyncRulesStorage
|
|
|
181
184
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
182
185
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
183
186
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
184
|
-
AND replica_id_columns = ${{ type: 'jsonb', value:
|
|
187
|
+
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
185
188
|
`
|
|
186
189
|
.decoded(models.SourceTable)
|
|
187
190
|
.first();
|
|
@@ -208,7 +211,7 @@ export class PostgresSyncRulesStorage
|
|
|
208
211
|
${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
|
|
209
212
|
${{ type: 'varchar', value: schema }},
|
|
210
213
|
${{ type: 'varchar', value: table }},
|
|
211
|
-
${{ type: 'jsonb', value:
|
|
214
|
+
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
212
215
|
)
|
|
213
216
|
RETURNING
|
|
214
217
|
*
|
|
@@ -218,15 +221,15 @@ export class PostgresSyncRulesStorage
|
|
|
218
221
|
sourceTableRow = row;
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
const sourceTable = new storage.SourceTable(
|
|
222
|
-
sourceTableRow!.id,
|
|
223
|
-
connection_tag,
|
|
224
|
-
objectId,
|
|
225
|
-
schema,
|
|
226
|
-
table,
|
|
227
|
-
|
|
228
|
-
sourceTableRow!.snapshot_done ?? true
|
|
229
|
-
);
|
|
224
|
+
const sourceTable = new storage.SourceTable({
|
|
225
|
+
id: sourceTableRow!.id,
|
|
226
|
+
connectionTag: connection_tag,
|
|
227
|
+
objectId: objectId,
|
|
228
|
+
schema: schema,
|
|
229
|
+
name: table,
|
|
230
|
+
replicaIdColumns: replicaIdColumns,
|
|
231
|
+
snapshotComplete: sourceTableRow!.snapshot_done ?? true
|
|
232
|
+
});
|
|
230
233
|
if (!sourceTable.snapshotComplete) {
|
|
231
234
|
sourceTable.snapshotStatus = {
|
|
232
235
|
totalEstimatedCount: Number(sourceTableRow!.snapshot_total_estimated_count ?? -1n),
|
|
@@ -284,19 +287,20 @@ export class PostgresSyncRulesStorage
|
|
|
284
287
|
table: sourceTable,
|
|
285
288
|
dropTables: truncatedTables.map(
|
|
286
289
|
(doc) =>
|
|
287
|
-
new storage.SourceTable(
|
|
288
|
-
doc.id,
|
|
289
|
-
connection_tag,
|
|
290
|
-
doc.relation_id?.object_id ?? 0,
|
|
291
|
-
doc.schema_name,
|
|
292
|
-
doc.table_name,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
290
|
+
new storage.SourceTable({
|
|
291
|
+
id: doc.id,
|
|
292
|
+
connectionTag: connection_tag,
|
|
293
|
+
objectId: doc.relation_id?.object_id ?? 0,
|
|
294
|
+
schema: doc.schema_name,
|
|
295
|
+
name: doc.table_name,
|
|
296
|
+
replicaIdColumns:
|
|
297
|
+
doc.replica_id_columns?.map((c) => ({
|
|
298
|
+
name: c.name,
|
|
299
|
+
typeOid: c.typeId,
|
|
300
|
+
type: c.type
|
|
301
|
+
})) ?? [],
|
|
302
|
+
snapshotComplete: doc.snapshot_done ?? true
|
|
303
|
+
})
|
|
300
304
|
)
|
|
301
305
|
};
|
|
302
306
|
});
|
|
@@ -310,13 +314,14 @@ export class PostgresSyncRulesStorage
|
|
|
310
314
|
SELECT
|
|
311
315
|
last_checkpoint_lsn,
|
|
312
316
|
no_checkpoint_before,
|
|
313
|
-
keepalive_op
|
|
317
|
+
keepalive_op,
|
|
318
|
+
snapshot_lsn
|
|
314
319
|
FROM
|
|
315
320
|
sync_rules
|
|
316
321
|
WHERE
|
|
317
322
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
318
323
|
`
|
|
319
|
-
.decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op']))
|
|
324
|
+
.decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op', 'snapshot_lsn']))
|
|
320
325
|
.first();
|
|
321
326
|
|
|
322
327
|
const checkpoint_lsn = syncRules?.last_checkpoint_lsn ?? null;
|
|
@@ -330,6 +335,7 @@ export class PostgresSyncRulesStorage
|
|
|
330
335
|
last_checkpoint_lsn: checkpoint_lsn,
|
|
331
336
|
keep_alive_op: syncRules?.keepalive_op,
|
|
332
337
|
no_checkpoint_before_lsn: syncRules?.no_checkpoint_before ?? options.zeroLSN,
|
|
338
|
+
resumeFromLsn: maxLsn(syncRules?.snapshot_lsn, checkpoint_lsn),
|
|
333
339
|
store_current_data: options.storeCurrentData,
|
|
334
340
|
skip_existing_rows: options.skipExistingRows ?? false,
|
|
335
341
|
batch_limits: this.options.batchLimits,
|
|
@@ -347,7 +353,7 @@ export class PostgresSyncRulesStorage
|
|
|
347
353
|
}
|
|
348
354
|
|
|
349
355
|
async getParameterSets(
|
|
350
|
-
checkpoint:
|
|
356
|
+
checkpoint: ReplicationCheckpoint,
|
|
351
357
|
lookups: sync_rules.ParameterLookup[]
|
|
352
358
|
): Promise<sync_rules.SqliteJsonRow[]> {
|
|
353
359
|
const rows = await this.db.sql`
|
|
@@ -370,7 +376,7 @@ export class PostgresSyncRulesStorage
|
|
|
370
376
|
value: lookups.map((l) => storage.serializeLookupBuffer(l).toString('hex'))
|
|
371
377
|
}}) AS FILTER
|
|
372
378
|
)
|
|
373
|
-
AND id <= ${{ type: 'int8', value: checkpoint }}
|
|
379
|
+
AND id <= ${{ type: 'int8', value: checkpoint.checkpoint }}
|
|
374
380
|
ORDER BY
|
|
375
381
|
lookup,
|
|
376
382
|
source_table,
|
|
@@ -644,43 +650,6 @@ export class PostgresSyncRulesStorage
|
|
|
644
650
|
`.execute();
|
|
645
651
|
}
|
|
646
652
|
|
|
647
|
-
async autoActivate(): Promise<void> {
|
|
648
|
-
await this.db.transaction(async (db) => {
|
|
649
|
-
const syncRulesRow = await db.sql`
|
|
650
|
-
SELECT
|
|
651
|
-
state
|
|
652
|
-
FROM
|
|
653
|
-
sync_rules
|
|
654
|
-
WHERE
|
|
655
|
-
id = ${{ type: 'int4', value: this.group_id }}
|
|
656
|
-
`
|
|
657
|
-
.decoded(pick(models.SyncRules, ['state']))
|
|
658
|
-
.first();
|
|
659
|
-
|
|
660
|
-
if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING) {
|
|
661
|
-
await db.sql`
|
|
662
|
-
UPDATE sync_rules
|
|
663
|
-
SET
|
|
664
|
-
state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
|
|
665
|
-
WHERE
|
|
666
|
-
id = ${{ type: 'int4', value: this.group_id }}
|
|
667
|
-
`.execute();
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
await db.sql`
|
|
671
|
-
UPDATE sync_rules
|
|
672
|
-
SET
|
|
673
|
-
state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
|
|
674
|
-
WHERE
|
|
675
|
-
(
|
|
676
|
-
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
677
|
-
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
678
|
-
)
|
|
679
|
-
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
680
|
-
`.execute();
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
653
|
private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
|
|
685
654
|
if (batch.length == 0) {
|
|
686
655
|
return new Map();
|
|
@@ -867,9 +836,18 @@ export class PostgresSyncRulesStorage
|
|
|
867
836
|
}
|
|
868
837
|
|
|
869
838
|
private makeActiveCheckpoint(row: models.ActiveCheckpointDecoded | null) {
|
|
870
|
-
return
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
839
|
+
return new PostgresReplicationCheckpoint(this, row?.last_checkpoint ?? 0n, row?.last_checkpoint_lsn ?? null);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
class PostgresReplicationCheckpoint implements storage.ReplicationCheckpoint {
|
|
844
|
+
constructor(
|
|
845
|
+
private storage: PostgresSyncRulesStorage,
|
|
846
|
+
public readonly checkpoint: utils.InternalOpId,
|
|
847
|
+
public readonly lsn: string | null
|
|
848
|
+
) {}
|
|
849
|
+
|
|
850
|
+
getParameterSets(lookups: sync_rules.ParameterLookup[]): Promise<sync_rules.SqliteJsonRow[]> {
|
|
851
|
+
return this.storage.getParameterSets(this, lookups);
|
|
874
852
|
}
|
|
875
853
|
}
|
|
@@ -31,6 +31,7 @@ export interface PostgresBucketBatchOptions {
|
|
|
31
31
|
no_checkpoint_before_lsn: string;
|
|
32
32
|
store_current_data: boolean;
|
|
33
33
|
keep_alive_op?: InternalOpId | null;
|
|
34
|
+
resumeFromLsn: string | null;
|
|
34
35
|
/**
|
|
35
36
|
* Set to true for initial replication.
|
|
36
37
|
*/
|
|
@@ -61,6 +62,8 @@ export class PostgresBucketBatch
|
|
|
61
62
|
|
|
62
63
|
public last_flushed_op: InternalOpId | null = null;
|
|
63
64
|
|
|
65
|
+
public resumeFromLsn: string | null;
|
|
66
|
+
|
|
64
67
|
protected db: lib_postgres.DatabaseClient;
|
|
65
68
|
protected group_id: number;
|
|
66
69
|
protected last_checkpoint_lsn: string | null;
|
|
@@ -73,6 +76,7 @@ export class PostgresBucketBatch
|
|
|
73
76
|
protected batch: OperationBatch | null;
|
|
74
77
|
private lastWaitingLogThrottled = 0;
|
|
75
78
|
private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
|
|
79
|
+
private needsActivation = true;
|
|
76
80
|
|
|
77
81
|
constructor(protected options: PostgresBucketBatchOptions) {
|
|
78
82
|
super();
|
|
@@ -81,6 +85,7 @@ export class PostgresBucketBatch
|
|
|
81
85
|
this.group_id = options.group_id;
|
|
82
86
|
this.last_checkpoint_lsn = options.last_checkpoint_lsn;
|
|
83
87
|
this.no_checkpoint_before_lsn = options.no_checkpoint_before_lsn;
|
|
88
|
+
this.resumeFromLsn = options.resumeFromLsn;
|
|
84
89
|
this.write_checkpoint_batch = [];
|
|
85
90
|
this.sync_rules = options.sync_rules;
|
|
86
91
|
this.markRecordUnavailable = options.markRecordUnavailable;
|
|
@@ -321,6 +326,7 @@ export class PostgresBucketBatch
|
|
|
321
326
|
// Don't create a checkpoint if there were no changes
|
|
322
327
|
if (!createEmptyCheckpoints && this.persisted_op == null) {
|
|
323
328
|
// Nothing to commit - return true
|
|
329
|
+
await this.autoActivate(lsn);
|
|
324
330
|
return true;
|
|
325
331
|
}
|
|
326
332
|
|
|
@@ -363,6 +369,7 @@ export class PostgresBucketBatch
|
|
|
363
369
|
.decoded(StatefulCheckpoint)
|
|
364
370
|
.first();
|
|
365
371
|
|
|
372
|
+
await this.autoActivate(lsn);
|
|
366
373
|
await notifySyncRulesUpdate(this.db, doc!);
|
|
367
374
|
|
|
368
375
|
this.persisted_op = null;
|
|
@@ -406,13 +413,14 @@ export class PostgresBucketBatch
|
|
|
406
413
|
.decoded(StatefulCheckpoint)
|
|
407
414
|
.first();
|
|
408
415
|
|
|
416
|
+
await this.autoActivate(lsn);
|
|
409
417
|
await notifySyncRulesUpdate(this.db, updated!);
|
|
410
418
|
|
|
411
419
|
this.last_checkpoint_lsn = lsn;
|
|
412
420
|
return true;
|
|
413
421
|
}
|
|
414
422
|
|
|
415
|
-
async
|
|
423
|
+
async setResumeLsn(lsn: string): Promise<void> {
|
|
416
424
|
await this.db.sql`
|
|
417
425
|
UPDATE sync_rules
|
|
418
426
|
SET
|
|
@@ -459,15 +467,15 @@ export class PostgresBucketBatch
|
|
|
459
467
|
}
|
|
460
468
|
});
|
|
461
469
|
return tables.map((table) => {
|
|
462
|
-
const copy = new storage.SourceTable(
|
|
463
|
-
table.id,
|
|
464
|
-
table.connectionTag,
|
|
465
|
-
table.objectId,
|
|
466
|
-
table.schema,
|
|
467
|
-
table.
|
|
468
|
-
table.replicaIdColumns,
|
|
469
|
-
table.snapshotComplete
|
|
470
|
-
);
|
|
470
|
+
const copy = new storage.SourceTable({
|
|
471
|
+
id: table.id,
|
|
472
|
+
connectionTag: table.connectionTag,
|
|
473
|
+
objectId: table.objectId,
|
|
474
|
+
schema: table.schema,
|
|
475
|
+
name: table.name,
|
|
476
|
+
replicaIdColumns: table.replicaIdColumns,
|
|
477
|
+
snapshotComplete: table.snapshotComplete
|
|
478
|
+
});
|
|
471
479
|
copy.syncData = table.syncData;
|
|
472
480
|
copy.syncParameters = table.syncParameters;
|
|
473
481
|
return copy;
|
|
@@ -916,6 +924,59 @@ export class PostgresBucketBatch
|
|
|
916
924
|
return result;
|
|
917
925
|
}
|
|
918
926
|
|
|
927
|
+
/**
|
|
928
|
+
* Switch from processing -> active if relevant.
|
|
929
|
+
*
|
|
930
|
+
* Called on new commits.
|
|
931
|
+
*/
|
|
932
|
+
private async autoActivate(lsn: string): Promise<void> {
|
|
933
|
+
if (!this.needsActivation) {
|
|
934
|
+
// Already activated
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let didActivate = false;
|
|
939
|
+
await this.db.transaction(async (db) => {
|
|
940
|
+
const syncRulesRow = await db.sql`
|
|
941
|
+
SELECT
|
|
942
|
+
state
|
|
943
|
+
FROM
|
|
944
|
+
sync_rules
|
|
945
|
+
WHERE
|
|
946
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
947
|
+
`
|
|
948
|
+
.decoded(pick(models.SyncRules, ['state']))
|
|
949
|
+
.first();
|
|
950
|
+
|
|
951
|
+
if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING) {
|
|
952
|
+
await db.sql`
|
|
953
|
+
UPDATE sync_rules
|
|
954
|
+
SET
|
|
955
|
+
state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
|
|
956
|
+
WHERE
|
|
957
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
958
|
+
`.execute();
|
|
959
|
+
didActivate = true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
await db.sql`
|
|
963
|
+
UPDATE sync_rules
|
|
964
|
+
SET
|
|
965
|
+
state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
|
|
966
|
+
WHERE
|
|
967
|
+
(
|
|
968
|
+
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
969
|
+
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
970
|
+
)
|
|
971
|
+
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
972
|
+
`.execute();
|
|
973
|
+
});
|
|
974
|
+
if (didActivate) {
|
|
975
|
+
this.logger.info(`Activated new sync rules at ${lsn}`);
|
|
976
|
+
}
|
|
977
|
+
this.needsActivation = false;
|
|
978
|
+
}
|
|
979
|
+
|
|
919
980
|
/**
|
|
920
981
|
* Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
|
|
921
982
|
* TODO maybe share this with an abstract class
|