@powersync/service-module-postgres-storage 0.0.0-dev-20250724091220 → 0.0.0-dev-20250724111728
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 +13 -6
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresReportStorageFactory.d.ts +21 -0
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +0 -1
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +10 -1
- package/dist/storage/PostgresReportStorageFactory.js +42 -0
- package/dist/storage/PostgresReportStorageFactory.js.map +1 -0
- package/dist/storage/PostgresStorageProvider.js +5 -1
- package/dist/storage/PostgresStorageProvider.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.js +5 -37
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.js +56 -1
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/package.json +7 -7
- package/src/storage/PostgresReportStorageFactory.ts +61 -0
- package/src/storage/PostgresStorageProvider.ts +7 -1
- package/src/storage/PostgresSyncRulesStorage.ts +5 -39
- package/src/storage/batch/PostgresBucketBatch.ts +62 -1
|
@@ -5,6 +5,7 @@ import { storage } from '@powersync/service-core';
|
|
|
5
5
|
import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js';
|
|
6
6
|
import { dropTables } from '../utils/db.js';
|
|
7
7
|
import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
|
|
8
|
+
import { PostgresReportStorageFactory } from './PostgresReportStorageFactory.js';
|
|
8
9
|
|
|
9
10
|
export class PostgresStorageProvider implements storage.BucketStorageProvider {
|
|
10
11
|
get type() {
|
|
@@ -28,9 +29,14 @@ export class PostgresStorageProvider implements storage.BucketStorageProvider {
|
|
|
28
29
|
config: normalizedConfig,
|
|
29
30
|
slot_name_prefix: options.resolvedConfig.slot_name_prefix
|
|
30
31
|
});
|
|
32
|
+
|
|
33
|
+
const reportStorageFactory = new PostgresReportStorageFactory({
|
|
34
|
+
config: normalizedConfig
|
|
35
|
+
});
|
|
36
|
+
|
|
31
37
|
return {
|
|
32
38
|
// TODO: IMPLEMENT REPORT STORAGE
|
|
33
|
-
reportStorage:
|
|
39
|
+
reportStorage: reportStorageFactory,
|
|
34
40
|
storage: storageFactory,
|
|
35
41
|
shutDown: async () => storageFactory.db[Symbol.asyncDispose](),
|
|
36
42
|
tearDown: async () => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
InternalOpId,
|
|
8
8
|
internalToExternalOpId,
|
|
9
9
|
LastValueSink,
|
|
10
|
+
maxLsn,
|
|
10
11
|
storage,
|
|
11
12
|
utils,
|
|
12
13
|
WatchWriteCheckpointOptions
|
|
@@ -310,13 +311,14 @@ export class PostgresSyncRulesStorage
|
|
|
310
311
|
SELECT
|
|
311
312
|
last_checkpoint_lsn,
|
|
312
313
|
no_checkpoint_before,
|
|
313
|
-
keepalive_op
|
|
314
|
+
keepalive_op,
|
|
315
|
+
snapshot_lsn
|
|
314
316
|
FROM
|
|
315
317
|
sync_rules
|
|
316
318
|
WHERE
|
|
317
319
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
318
320
|
`
|
|
319
|
-
.decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op']))
|
|
321
|
+
.decoded(pick(models.SyncRules, ['last_checkpoint_lsn', 'no_checkpoint_before', 'keepalive_op', 'snapshot_lsn']))
|
|
320
322
|
.first();
|
|
321
323
|
|
|
322
324
|
const checkpoint_lsn = syncRules?.last_checkpoint_lsn ?? null;
|
|
@@ -330,6 +332,7 @@ export class PostgresSyncRulesStorage
|
|
|
330
332
|
last_checkpoint_lsn: checkpoint_lsn,
|
|
331
333
|
keep_alive_op: syncRules?.keepalive_op,
|
|
332
334
|
no_checkpoint_before_lsn: syncRules?.no_checkpoint_before ?? options.zeroLSN,
|
|
335
|
+
resumeFromLsn: maxLsn(syncRules?.snapshot_lsn, checkpoint_lsn),
|
|
333
336
|
store_current_data: options.storeCurrentData,
|
|
334
337
|
skip_existing_rows: options.skipExistingRows ?? false,
|
|
335
338
|
batch_limits: this.options.batchLimits,
|
|
@@ -644,43 +647,6 @@ export class PostgresSyncRulesStorage
|
|
|
644
647
|
`.execute();
|
|
645
648
|
}
|
|
646
649
|
|
|
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
650
|
private async getChecksumsInternal(batch: storage.FetchPartialBucketChecksum[]): Promise<storage.PartialChecksumMap> {
|
|
685
651
|
if (batch.length == 0) {
|
|
686
652
|
return new Map();
|
|
@@ -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
|
|
@@ -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
|