@powersync/service-module-postgres-storage 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634
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 +125 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/migrations/scripts/1771424826685-current-data-pending-deletes.d.ts +3 -0
- package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +6 -1
- package/dist/@types/storage/PostgresCompactor.d.ts +8 -2
- package/dist/@types/storage/PostgresReportStorage.d.ts +1 -1
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +14 -6
- package/dist/@types/storage/batch/OperationBatch.d.ts +2 -2
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +14 -10
- package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +17 -5
- package/dist/@types/storage/current-data-store.d.ts +85 -0
- package/dist/@types/storage/table-id.d.ts +2 -0
- package/dist/@types/types/models/CurrentData.d.ts +18 -3
- package/dist/@types/types/models/SyncRules.d.ts +9 -0
- package/dist/@types/types/models/models-index.d.ts +1 -1
- package/dist/@types/utils/bson.d.ts +1 -1
- package/dist/@types/utils/test-utils.d.ts +3 -3
- package/dist/@types/utils/utils-index.d.ts +1 -1
- package/dist/migrations/migration-utils.js +2 -2
- package/dist/migrations/migration-utils.js.map +1 -1
- package/dist/migrations/scripts/1684951997326-init.js +24 -100
- package/dist/migrations/scripts/1684951997326-init.js.map +1 -1
- package/dist/migrations/scripts/1749024804042-snapshot-progress.js +12 -88
- package/dist/migrations/scripts/1749024804042-snapshot-progress.js.map +1 -1
- package/dist/migrations/scripts/1756282360128-connection-reporting.js +12 -88
- package/dist/migrations/scripts/1756282360128-connection-reporting.js.map +1 -1
- package/dist/migrations/scripts/1771232439485-storage-version.js +14 -90
- package/dist/migrations/scripts/1771232439485-storage-version.js.map +1 -1
- package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js +8 -0
- package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js.map +1 -0
- package/dist/migrations/scripts/1771491856000-sync-plan.js +6 -82
- package/dist/migrations/scripts/1771491856000-sync-plan.js.map +1 -1
- package/dist/storage/PostgresBucketStorageFactory.js +47 -9
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.js +14 -6
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresReportStorage.js +3 -3
- package/dist/storage/PostgresReportStorage.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.js +93 -60
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/OperationBatch.js +2 -1
- package/dist/storage/batch/OperationBatch.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.js +297 -215
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/storage/batch/PostgresPersistedBatch.js +86 -81
- package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/current-data-store.js +270 -0
- package/dist/storage/current-data-store.js.map +1 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +3 -3
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/table-id.js +8 -0
- package/dist/storage/table-id.js.map +1 -0
- package/dist/types/models/CurrentData.js +11 -2
- package/dist/types/models/CurrentData.js.map +1 -1
- package/dist/types/models/SyncRules.js +3 -1
- package/dist/types/models/SyncRules.js.map +1 -1
- package/dist/types/models/models-index.js +1 -1
- package/dist/types/models/models-index.js.map +1 -1
- package/dist/utils/bson.js.map +1 -1
- package/dist/utils/db.js +9 -0
- package/dist/utils/db.js.map +1 -1
- package/dist/utils/test-utils.js +39 -108
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/utils-index.js +1 -1
- package/dist/utils/utils-index.js.map +1 -1
- package/package.json +12 -13
- package/src/migrations/migration-utils.ts +2 -2
- package/src/migrations/scripts/1771424826685-current-data-pending-deletes.ts +10 -0
- package/src/storage/PostgresBucketStorageFactory.ts +60 -10
- package/src/storage/PostgresCompactor.ts +17 -8
- package/src/storage/PostgresReportStorage.ts +5 -5
- package/src/storage/PostgresSyncRulesStorage.ts +108 -65
- package/src/storage/batch/OperationBatch.ts +4 -3
- package/src/storage/batch/PostgresBucketBatch.ts +318 -240
- package/src/storage/batch/PostgresPersistedBatch.ts +92 -84
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +3 -1
- package/src/storage/current-data-store.ts +326 -0
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +3 -6
- package/src/storage/table-id.ts +9 -0
- package/src/types/models/CurrentData.ts +17 -4
- package/src/types/models/SyncRules.ts +3 -1
- package/src/types/models/models-index.ts +1 -1
- package/src/utils/bson.ts +1 -1
- package/src/utils/db.ts +10 -0
- package/src/utils/test-utils.ts +16 -9
- package/src/utils/utils-index.ts +1 -1
- package/test/src/__snapshots__/storage.test.ts.snap +151 -0
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1393 -16
- package/test/src/connection-report-storage.test.ts +4 -4
- package/test/src/env.ts +1 -1
- package/test/src/migrations.test.ts +1 -1
- package/test/src/setup.ts +1 -1
- package/test/src/storage.test.ts +138 -131
- package/test/src/storage_compacting.test.ts +80 -11
- package/test/src/storage_sync.test.ts +57 -54
- package/test/src/util.ts +4 -4
- package/test/tsconfig.json +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils-index.js","sourceRoot":"","sources":["../../src/utils/utils-index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC;AACxB,
|
|
1
|
+
{"version":3,"file":"utils-index.js","sourceRoot":"","sources":["../../src/utils/utils-index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC;AACxB,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,cAAc,eAAe,CAAC"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@powersync/service-module-postgres-storage",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
4
|
"types": "dist/@types/index.d.ts",
|
|
5
|
-
"version": "0.0.0-dev-
|
|
5
|
+
"version": "0.0.0-dev-20260511080634",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"license": "FSL-1.1-ALv2",
|
|
8
8
|
"type": "module",
|
|
@@ -26,25 +26,24 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"ix": "^5.0.0",
|
|
28
28
|
"lru-cache": "^10.2.2",
|
|
29
|
-
"p-defer": "^4.0.1",
|
|
30
29
|
"ts-codec": "^1.3.0",
|
|
31
|
-
"uuid": "^
|
|
32
|
-
"@powersync/lib-service-postgres": "0.0.0-dev-
|
|
33
|
-
"@powersync/lib-services-framework": "0.0.0-dev-
|
|
34
|
-
"@powersync/service-core": "0.0.0-dev-
|
|
35
|
-
"@powersync/service-types": "0.0.0-dev-
|
|
36
|
-
"@powersync/service-jpgwire": "0.0.0-dev-
|
|
37
|
-
"@powersync/service-jsonbig": "0.
|
|
38
|
-
"@powersync/service-sync-rules": "0.0.0-dev-
|
|
30
|
+
"uuid": "^14.0.0",
|
|
31
|
+
"@powersync/lib-service-postgres": "0.0.0-dev-20260511080634",
|
|
32
|
+
"@powersync/lib-services-framework": "0.0.0-dev-20260511080634",
|
|
33
|
+
"@powersync/service-core": "0.0.0-dev-20260511080634",
|
|
34
|
+
"@powersync/service-types": "0.0.0-dev-20260511080634",
|
|
35
|
+
"@powersync/service-jpgwire": "0.0.0-dev-20260511080634",
|
|
36
|
+
"@powersync/service-jsonbig": "0.0.0-dev-20260511080634",
|
|
37
|
+
"@powersync/service-sync-rules": "0.0.0-dev-20260511080634"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
|
-
"typescript": "
|
|
42
|
-
"@powersync/service-core-tests": "0.0.0-dev-
|
|
40
|
+
"typescript": "~6.0.3",
|
|
41
|
+
"@powersync/service-core-tests": "0.0.0-dev-20260511080634"
|
|
43
42
|
},
|
|
44
43
|
"scripts": {
|
|
45
44
|
"build": "tsc -b",
|
|
46
45
|
"build:tests": "tsc -b test/tsconfig.json",
|
|
47
|
-
"clean": "rm -rf ./
|
|
46
|
+
"clean": "rm -rf ./dist && tsc -b --clean",
|
|
48
47
|
"test": "vitest"
|
|
49
48
|
}
|
|
50
49
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
3
|
import { configFile } from '@powersync/service-types';
|
|
3
4
|
import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js';
|
|
4
|
-
import { STORAGE_SCHEMA_NAME } from '../utils/db.js';
|
|
5
|
-
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
6
5
|
import { getStorageApplicationName } from '../utils/application-name.js';
|
|
6
|
+
import { STORAGE_SCHEMA_NAME } from '../utils/db.js';
|
|
7
7
|
|
|
8
8
|
export const openMigrationDB = (config: configFile.BaseStorageConfig) => {
|
|
9
9
|
if (!isPostgresStorageConfig(config)) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { migrations } from '@powersync/service-core';
|
|
2
|
+
|
|
3
|
+
export const up: migrations.PowerSyncMigrationFunction = async (_context) => {
|
|
4
|
+
// No-op.
|
|
5
|
+
// Pending-delete support is now storage-version specific and initialized when v3 sync config is deployed.
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const down: migrations.PowerSyncMigrationFunction = async (_context) => {
|
|
9
|
+
// No-op.
|
|
10
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GetIntanceOptions, storage, SyncRulesBucketStorage } from '@powersync/service-core';
|
|
1
|
+
import { framework, GetIntanceOptions, storage, SyncRulesBucketStorage } from '@powersync/service-core';
|
|
2
2
|
import * as pg_wire from '@powersync/service-jpgwire';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import * as uuid from 'uuid';
|
|
@@ -6,11 +6,11 @@ import * as uuid from 'uuid';
|
|
|
6
6
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
7
7
|
import { models, NormalizedPostgresStorageConfig } from '../types/types.js';
|
|
8
8
|
|
|
9
|
+
import { getStorageApplicationName } from '../utils/application-name.js';
|
|
9
10
|
import { NOTIFICATION_CHANNEL, STORAGE_SCHEMA_NAME } from '../utils/db.js';
|
|
10
11
|
import { notifySyncRulesUpdate } from './batch/PostgresBucketBatch.js';
|
|
11
12
|
import { PostgresSyncRulesStorage } from './PostgresSyncRulesStorage.js';
|
|
12
13
|
import { PostgresPersistedSyncRulesContent } from './sync-rules/PostgresPersistedSyncRulesContent.js';
|
|
13
|
-
import { getStorageApplicationName } from '../utils/application-name.js';
|
|
14
14
|
|
|
15
15
|
export type PostgresBucketStorageOptions = {
|
|
16
16
|
config: NormalizedPostgresStorageConfig;
|
|
@@ -18,6 +18,7 @@ export type PostgresBucketStorageOptions = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
21
|
+
[framework.DO_NOT_LOG] = true;
|
|
21
22
|
readonly db: lib_postgres.DatabaseClient;
|
|
22
23
|
public readonly slot_name_prefix: string;
|
|
23
24
|
|
|
@@ -82,15 +83,27 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
82
83
|
|
|
83
84
|
const sizes = await this.db.sql`
|
|
84
85
|
SELECT
|
|
85
|
-
|
|
86
|
+
COALESCE(
|
|
87
|
+
pg_total_relation_size(to_regclass('current_data')),
|
|
88
|
+
0
|
|
89
|
+
) AS v1_current_size_bytes,
|
|
90
|
+
COALESCE(
|
|
91
|
+
pg_total_relation_size(to_regclass('v3_current_data')),
|
|
92
|
+
0
|
|
93
|
+
) AS v3_current_size_bytes,
|
|
86
94
|
pg_total_relation_size('bucket_parameters') AS parameter_size_bytes,
|
|
87
95
|
pg_total_relation_size('bucket_data') AS operations_size_bytes;
|
|
88
|
-
`.first<{
|
|
96
|
+
`.first<{
|
|
97
|
+
v1_current_size_bytes: bigint;
|
|
98
|
+
v3_current_size_bytes: bigint;
|
|
99
|
+
parameter_size_bytes: bigint;
|
|
100
|
+
operations_size_bytes: bigint;
|
|
101
|
+
}>();
|
|
89
102
|
|
|
90
103
|
return {
|
|
91
104
|
operations_size_bytes: Number(sizes!.operations_size_bytes),
|
|
92
105
|
parameters_size_bytes: Number(sizes!.parameter_size_bytes),
|
|
93
|
-
replication_size_bytes: Number(sizes!.
|
|
106
|
+
replication_size_bytes: Number(sizes!.v1_current_size_bytes) + Number(sizes!.v3_current_size_bytes)
|
|
94
107
|
};
|
|
95
108
|
}
|
|
96
109
|
|
|
@@ -141,7 +154,16 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<PostgresPersistedSyncRulesContent> {
|
|
144
|
-
const storageVersion =
|
|
157
|
+
const storageVersion =
|
|
158
|
+
options.storageVersion ?? options.config.parsed.config.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
|
|
159
|
+
const storageConfig = storage.STORAGE_VERSION_CONFIG[storageVersion];
|
|
160
|
+
if (storageConfig == null) {
|
|
161
|
+
throw new framework.ServiceError(
|
|
162
|
+
framework.ErrorCode.PSYNC_S1005,
|
|
163
|
+
`Unsupported storage version ${storageVersion}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
await this.initializeStorageVersion(storageConfig);
|
|
145
167
|
return this.db.transaction(async (db) => {
|
|
146
168
|
await db.sql`
|
|
147
169
|
UPDATE sync_rules
|
|
@@ -202,14 +224,42 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
202
224
|
});
|
|
203
225
|
}
|
|
204
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Lazy-initializes storage-version-specific structures, if needed.
|
|
229
|
+
*/
|
|
230
|
+
private async initializeStorageVersion(storageConfig: storage.StorageVersionConfig) {
|
|
231
|
+
if (!storageConfig.softDeleteCurrentData) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await this.db.sql`
|
|
236
|
+
CREATE TABLE IF NOT EXISTS v3_current_data (
|
|
237
|
+
group_id integer NOT NULL,
|
|
238
|
+
source_table TEXT NOT NULL,
|
|
239
|
+
source_key bytea NOT NULL,
|
|
240
|
+
CONSTRAINT unique_v3_current_data_id PRIMARY KEY (group_id, source_table, source_key),
|
|
241
|
+
buckets jsonb NOT NULL,
|
|
242
|
+
data bytea NOT NULL,
|
|
243
|
+
lookups bytea[] NOT NULL,
|
|
244
|
+
pending_delete BIGINT NULL
|
|
245
|
+
)
|
|
246
|
+
`.execute();
|
|
247
|
+
|
|
248
|
+
await this.db.sql`
|
|
249
|
+
CREATE INDEX IF NOT EXISTS v3_current_data_pending_deletes ON v3_current_data (group_id, pending_delete)
|
|
250
|
+
WHERE
|
|
251
|
+
pending_delete IS NOT NULL
|
|
252
|
+
`.execute();
|
|
253
|
+
}
|
|
254
|
+
|
|
205
255
|
async restartReplication(sync_rules_group_id: number): Promise<void> {
|
|
206
256
|
const next = await this.getNextSyncRulesContent();
|
|
207
257
|
const active = await this.getActiveSyncRulesContent();
|
|
208
258
|
|
|
209
|
-
// In both the below cases, we create a new
|
|
259
|
+
// In both the below cases, we create a new replication stream.
|
|
210
260
|
// The current one will continue serving sync requests until the next one has finished processing.
|
|
211
261
|
if (next != null && next.id == sync_rules_group_id) {
|
|
212
|
-
// We need to redo the "next"
|
|
262
|
+
// We need to redo the "next" replication stream
|
|
213
263
|
|
|
214
264
|
await this.updateSyncRules(next.asUpdateOptions());
|
|
215
265
|
// Pro-actively stop replicating
|
|
@@ -222,7 +272,7 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
222
272
|
AND state = ${{ value: storage.SyncRuleState.PROCESSING, type: 'varchar' }}
|
|
223
273
|
`.execute();
|
|
224
274
|
} else if (next == null && active?.id == sync_rules_group_id) {
|
|
225
|
-
// Slot removed for "active"
|
|
275
|
+
// Slot removed for "active" replication stream, while there is no "next" one.
|
|
226
276
|
await this.updateSyncRules(active.asUpdateOptions());
|
|
227
277
|
|
|
228
278
|
// Pro-actively stop replicating, but still serve clients with existing data
|
|
@@ -235,7 +285,7 @@ export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
|
235
285
|
AND state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
236
286
|
`.execute();
|
|
237
287
|
} else if (next != null && active?.id == sync_rules_group_id) {
|
|
238
|
-
// Already have "next"
|
|
288
|
+
// Already have "next" replication stream - don't update any.
|
|
239
289
|
|
|
240
290
|
// Pro-actively stop replicating, but still serve clients with existing data
|
|
241
291
|
await this.db.sql`
|
|
@@ -51,20 +51,20 @@ export class PostgresCompactor {
|
|
|
51
51
|
private moveBatchLimit: number;
|
|
52
52
|
private moveBatchQueryLimit: number;
|
|
53
53
|
private clearBatchLimit: number;
|
|
54
|
-
private maxOpId: InternalOpId
|
|
54
|
+
private maxOpId: InternalOpId;
|
|
55
55
|
private buckets: string[] | undefined;
|
|
56
56
|
|
|
57
57
|
constructor(
|
|
58
58
|
private db: lib_postgres.DatabaseClient,
|
|
59
59
|
private group_id: number,
|
|
60
|
-
options
|
|
60
|
+
options: PostgresCompactOptions
|
|
61
61
|
) {
|
|
62
|
-
this.idLimitBytes = (options
|
|
63
|
-
this.moveBatchLimit = options
|
|
64
|
-
this.moveBatchQueryLimit = options
|
|
65
|
-
this.clearBatchLimit = options
|
|
66
|
-
this.maxOpId = options
|
|
67
|
-
this.buckets = options
|
|
62
|
+
this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
|
|
63
|
+
this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
64
|
+
this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
65
|
+
this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
|
|
66
|
+
this.maxOpId = options.maxOpId ?? 0n;
|
|
67
|
+
this.buckets = options.compactBuckets;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -240,6 +240,15 @@ export class PostgresCompactor {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Expose the internal clearBucket() method to tests.
|
|
245
|
+
*
|
|
246
|
+
* @deprecated Only for tests
|
|
247
|
+
*/
|
|
248
|
+
clearBucketForTests(bucket: string, op: InternalOpId) {
|
|
249
|
+
return this.clearBucket(bucket, op);
|
|
250
|
+
}
|
|
251
|
+
|
|
243
252
|
/**
|
|
244
253
|
* Perform a CLEAR compact for a bucket.
|
|
245
254
|
*
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
|
+
import { logger } from '@powersync/lib-services-framework';
|
|
1
3
|
import { storage } from '@powersync/service-core';
|
|
2
4
|
import * as pg_wire from '@powersync/service-jpgwire';
|
|
3
5
|
import { event_types } from '@powersync/service-types';
|
|
6
|
+
import { ClientConnectionResponse } from '@powersync/service-types/dist/reports.js';
|
|
7
|
+
import { toInteger } from 'ix/util/tointeger.js';
|
|
4
8
|
import { v4 } from 'uuid';
|
|
5
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
6
|
-
import { NormalizedPostgresStorageConfig } from '../types/types.js';
|
|
7
9
|
import { SdkReporting, SdkReportingDecoded } from '../types/models/SdkReporting.js';
|
|
8
|
-
import {
|
|
9
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
10
|
+
import { NormalizedPostgresStorageConfig } from '../types/types.js';
|
|
10
11
|
import { getStorageApplicationName } from '../utils/application-name.js';
|
|
11
12
|
import { STORAGE_SCHEMA_NAME } from '../utils/db.js';
|
|
12
|
-
import { ClientConnectionResponse } from '@powersync/service-types/dist/reports.js';
|
|
13
13
|
|
|
14
14
|
export type PostgresReportStorageOptions = {
|
|
15
15
|
config: NormalizedPostgresStorageConfig;
|
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
internalToExternalOpId,
|
|
10
10
|
LastValueSink,
|
|
11
11
|
maxLsn,
|
|
12
|
+
ParameterSetLimitExceededError,
|
|
12
13
|
PartialChecksum,
|
|
13
14
|
PopulateChecksumCacheOptions,
|
|
14
15
|
PopulateChecksumCacheResults,
|
|
15
16
|
ReplicationCheckpoint,
|
|
16
17
|
storage,
|
|
18
|
+
StorageVersionConfig,
|
|
17
19
|
utils,
|
|
18
20
|
WatchWriteCheckpointOptions
|
|
19
21
|
} from '@powersync/service-core';
|
|
@@ -33,6 +35,7 @@ import { SourceTableDecoded, StoredRelationId } from '../types/models/SourceTabl
|
|
|
33
35
|
import { pick } from '../utils/ts-codec.js';
|
|
34
36
|
import { PostgresBucketBatch } from './batch/PostgresBucketBatch.js';
|
|
35
37
|
import { PostgresWriteCheckpointAPI } from './checkpoints/PostgresWriteCheckpointAPI.js';
|
|
38
|
+
import { PostgresCurrentDataStore } from './current-data-store.js';
|
|
36
39
|
import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
|
|
37
40
|
import { PostgresCompactor } from './PostgresCompactor.js';
|
|
38
41
|
|
|
@@ -48,15 +51,20 @@ export class PostgresSyncRulesStorage
|
|
|
48
51
|
extends framework.BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
49
52
|
implements storage.SyncRulesBucketStorage
|
|
50
53
|
{
|
|
54
|
+
[framework.DO_NOT_LOG] = true;
|
|
55
|
+
|
|
51
56
|
public readonly group_id: number;
|
|
52
57
|
public readonly sync_rules: storage.PersistedSyncRulesContent;
|
|
53
58
|
public readonly slot_name: string;
|
|
54
59
|
public readonly factory: PostgresBucketStorageFactory;
|
|
60
|
+
public readonly storageConfig: StorageVersionConfig;
|
|
61
|
+
public readonly logger: framework.Logger;
|
|
55
62
|
|
|
56
63
|
private sharedIterator = new BroadcastIterable((signal) => this.watchActiveCheckpoint(signal));
|
|
57
64
|
|
|
58
65
|
protected db: lib_postgres.DatabaseClient;
|
|
59
66
|
protected writeCheckpointAPI: PostgresWriteCheckpointAPI;
|
|
67
|
+
private readonly currentDataStore: PostgresCurrentDataStore;
|
|
60
68
|
|
|
61
69
|
// TODO we might be able to share this in an abstract class
|
|
62
70
|
private parsedSyncRulesCache:
|
|
@@ -71,6 +79,9 @@ export class PostgresSyncRulesStorage
|
|
|
71
79
|
this.sync_rules = options.sync_rules;
|
|
72
80
|
this.slot_name = options.sync_rules.slot_name;
|
|
73
81
|
this.factory = options.factory;
|
|
82
|
+
this.storageConfig = options.sync_rules.getStorageConfig();
|
|
83
|
+
this.currentDataStore = new PostgresCurrentDataStore(this.storageConfig);
|
|
84
|
+
this.logger = options.sync_rules.logger;
|
|
74
85
|
|
|
75
86
|
this.writeCheckpointAPI = new PostgresWriteCheckpointAPI({
|
|
76
87
|
db: this.db,
|
|
@@ -100,8 +111,8 @@ export class PostgresSyncRulesStorage
|
|
|
100
111
|
getParsedSyncRules(options: storage.ParseSyncRulesOptions): sync_rules.HydratedSyncRules {
|
|
101
112
|
const { parsed, options: cachedOptions } = this.parsedSyncRulesCache ?? {};
|
|
102
113
|
/**
|
|
103
|
-
* Check if the cached sync
|
|
104
|
-
* Parse sync
|
|
114
|
+
* Check if the cached sync config, if present, had the same options.
|
|
115
|
+
* Parse sync config if the options are different or if there is no cached value.
|
|
105
116
|
*/
|
|
106
117
|
if (!parsed || options.defaultSchema != cachedOptions?.defaultSchema) {
|
|
107
118
|
this.parsedSyncRulesCache = { parsed: this.sync_rules.parsed(options).hydratedSyncRules(), options };
|
|
@@ -121,11 +132,22 @@ export class PostgresSyncRulesStorage
|
|
|
121
132
|
`.execute();
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
compact(options?: storage.CompactOptions): Promise<void> {
|
|
125
|
-
|
|
135
|
+
async compact(options?: storage.CompactOptions): Promise<void> {
|
|
136
|
+
let maxOpId = options?.maxOpId;
|
|
137
|
+
if (maxOpId == null) {
|
|
138
|
+
const checkpoint = await this.getCheckpoint();
|
|
139
|
+
// Note: If there is no active checkpoint, this will be 0, in which case no compacting is performed
|
|
140
|
+
maxOpId = checkpoint.checkpoint;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new PostgresCompactor(this.db, this.group_id, {
|
|
144
|
+
...options,
|
|
145
|
+
maxOpId,
|
|
146
|
+
logger: this.logger
|
|
147
|
+
}).compact();
|
|
126
148
|
}
|
|
127
149
|
|
|
128
|
-
async populatePersistentChecksumCache(
|
|
150
|
+
async populatePersistentChecksumCache(_options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults> {
|
|
129
151
|
// no-op - checksum cache is not implemented for Postgres yet
|
|
130
152
|
return { buckets: 0 };
|
|
131
153
|
}
|
|
@@ -327,10 +349,7 @@ export class PostgresSyncRulesStorage
|
|
|
327
349
|
});
|
|
328
350
|
}
|
|
329
351
|
|
|
330
|
-
async
|
|
331
|
-
options: storage.StartBatchOptions,
|
|
332
|
-
callback: (batch: storage.BucketStorageBatch) => Promise<void>
|
|
333
|
-
): Promise<storage.FlushedResult | null> {
|
|
352
|
+
async createWriter(options: storage.CreateWriterOptions): Promise<storage.BucketStorageBatch> {
|
|
334
353
|
const syncRules = await this.db.sql`
|
|
335
354
|
SELECT
|
|
336
355
|
last_checkpoint_lsn,
|
|
@@ -347,78 +366,105 @@ export class PostgresSyncRulesStorage
|
|
|
347
366
|
|
|
348
367
|
const checkpoint_lsn = syncRules?.last_checkpoint_lsn ?? null;
|
|
349
368
|
|
|
350
|
-
const
|
|
351
|
-
logger: options.logger ??
|
|
369
|
+
const writer = new PostgresBucketBatch({
|
|
370
|
+
logger: options.logger ?? this.logger,
|
|
352
371
|
db: this.db,
|
|
353
372
|
sync_rules: this.sync_rules.parsed(options).hydratedSyncRules(),
|
|
354
373
|
group_id: this.group_id,
|
|
355
374
|
slot_name: this.slot_name,
|
|
356
375
|
last_checkpoint_lsn: checkpoint_lsn,
|
|
357
376
|
keep_alive_op: syncRules?.keepalive_op,
|
|
358
|
-
no_checkpoint_before_lsn: syncRules?.no_checkpoint_before ?? options.zeroLSN,
|
|
359
377
|
resumeFromLsn: maxLsn(syncRules?.snapshot_lsn, checkpoint_lsn),
|
|
360
378
|
store_current_data: options.storeCurrentData,
|
|
361
379
|
skip_existing_rows: options.skipExistingRows ?? false,
|
|
362
380
|
batch_limits: this.options.batchLimits,
|
|
363
|
-
markRecordUnavailable: options.markRecordUnavailable
|
|
381
|
+
markRecordUnavailable: options.markRecordUnavailable,
|
|
382
|
+
storageConfig: this.storageConfig
|
|
364
383
|
});
|
|
365
|
-
this.iterateListeners((cb) => cb.batchStarted?.(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
384
|
+
this.iterateListeners((cb) => cb.batchStarted?.(writer));
|
|
385
|
+
return writer;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @deprecated Use `createWriter()` with `await using` instead.
|
|
390
|
+
*/
|
|
391
|
+
async startBatch(
|
|
392
|
+
options: storage.CreateWriterOptions,
|
|
393
|
+
callback: (batch: storage.BucketStorageBatch) => Promise<void>
|
|
394
|
+
): Promise<storage.FlushedResult | null> {
|
|
395
|
+
await using writer = await this.createWriter(options);
|
|
396
|
+
await callback(writer);
|
|
397
|
+
await writer.flush();
|
|
398
|
+
return writer.last_flushed_op != null ? { flushed_op: writer.last_flushed_op } : null;
|
|
374
399
|
}
|
|
375
400
|
|
|
376
401
|
async getParameterSets(
|
|
377
402
|
checkpoint: ReplicationCheckpoint,
|
|
378
|
-
lookups: sync_rules.ScopedParameterLookup[]
|
|
403
|
+
lookups: sync_rules.ScopedParameterLookup[],
|
|
404
|
+
limit: number
|
|
379
405
|
): Promise<sync_rules.SqliteJsonRow[]> {
|
|
380
406
|
const rows = await this.db.sql`
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
WHERE
|
|
390
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
391
|
-
AND lookup = ANY (
|
|
392
|
-
SELECT
|
|
393
|
-
decode((FILTER ->> 0)::text, 'hex') -- Decode the hex string to bytea
|
|
407
|
+
WITH
|
|
408
|
+
rows AS (
|
|
409
|
+
SELECT DISTINCT
|
|
410
|
+
ON (lookup, source_table, source_key) lookup,
|
|
411
|
+
source_table,
|
|
412
|
+
source_key,
|
|
413
|
+
id,
|
|
414
|
+
bucket_parameters
|
|
394
415
|
FROM
|
|
395
|
-
|
|
416
|
+
bucket_parameters
|
|
417
|
+
WHERE
|
|
418
|
+
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
419
|
+
AND lookup = ANY (
|
|
420
|
+
SELECT
|
|
421
|
+
decode((FILTER ->> 0)::text, 'hex') -- Decode the hex string to bytea
|
|
422
|
+
FROM
|
|
423
|
+
jsonb_array_elements(${{
|
|
396
424
|
type: 'jsonb',
|
|
397
425
|
value: lookups.map((l) => storage.serializeLookupBuffer(l).toString('hex'))
|
|
398
426
|
}}) AS FILTER
|
|
427
|
+
)
|
|
428
|
+
AND id <= ${{ type: 'int8', value: checkpoint.checkpoint }}
|
|
429
|
+
ORDER BY
|
|
430
|
+
lookup,
|
|
431
|
+
source_table,
|
|
432
|
+
source_key,
|
|
433
|
+
id DESC
|
|
399
434
|
)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
435
|
+
SELECT
|
|
436
|
+
bucket_parameters
|
|
437
|
+
FROM
|
|
438
|
+
rows
|
|
439
|
+
WHERE
|
|
440
|
+
bucket_parameters != '[]'
|
|
441
|
+
LIMIT
|
|
442
|
+
${{ type: 'int4', value: limit + 1 }}
|
|
406
443
|
`
|
|
407
444
|
.decoded(pick(models.BucketParameters, ['bucket_parameters']))
|
|
408
445
|
.rows();
|
|
409
446
|
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
447
|
+
const parameters = rows
|
|
448
|
+
.map((row) => {
|
|
449
|
+
return JSONBig.parse(row.bucket_parameters) as sync_rules.SqliteJsonRow[];
|
|
450
|
+
})
|
|
451
|
+
.flat();
|
|
452
|
+
|
|
453
|
+
if (parameters.length > limit) {
|
|
454
|
+
// Note that the LIMIT in the query allows more rows than parameters (because each row stores an array of
|
|
455
|
+
// parameter results). That array is very small though, and it doesn't allow fewer rows (due to the != []), so
|
|
456
|
+
// the SQL limit is good enough.
|
|
457
|
+
throw new ParameterSetLimitExceededError(limit);
|
|
458
|
+
}
|
|
459
|
+
return parameters;
|
|
414
460
|
}
|
|
415
461
|
|
|
416
462
|
async *getBucketDataBatch(
|
|
417
463
|
checkpoint: InternalOpId,
|
|
418
|
-
dataBuckets:
|
|
464
|
+
dataBuckets: storage.BucketDataRequest[],
|
|
419
465
|
options?: storage.BucketDataBatchOptions
|
|
420
466
|
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
421
|
-
if (dataBuckets.
|
|
467
|
+
if (dataBuckets.length == 0) {
|
|
422
468
|
return;
|
|
423
469
|
}
|
|
424
470
|
|
|
@@ -430,10 +476,8 @@ export class PostgresSyncRulesStorage
|
|
|
430
476
|
// not match up with chunks.
|
|
431
477
|
|
|
432
478
|
const end = checkpoint ?? BIGINT_MAX;
|
|
433
|
-
const filters =
|
|
434
|
-
|
|
435
|
-
start: start
|
|
436
|
-
}));
|
|
479
|
+
const filters = dataBuckets.map((request) => ({ bucket_name: request.bucket, start: request.start }));
|
|
480
|
+
const startOpByBucket = new Map(dataBuckets.map((request) => [request.bucket, request.start]));
|
|
437
481
|
|
|
438
482
|
const batchRowLimit = options?.limit ?? storage.DEFAULT_DOCUMENT_BATCH_LIMIT;
|
|
439
483
|
const chunkSizeLimitBytes = options?.chunkLimitBytes ?? storage.DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES;
|
|
@@ -533,7 +577,7 @@ export class PostgresSyncRulesStorage
|
|
|
533
577
|
}
|
|
534
578
|
|
|
535
579
|
if (start == null) {
|
|
536
|
-
const startOpId =
|
|
580
|
+
const startOpId = startOpByBucket.get(bucket_name);
|
|
537
581
|
if (startOpId == null) {
|
|
538
582
|
throw new framework.ServiceAssertionError(`data for unexpected bucket: ${bucket_name}`);
|
|
539
583
|
}
|
|
@@ -588,7 +632,10 @@ export class PostgresSyncRulesStorage
|
|
|
588
632
|
}
|
|
589
633
|
}
|
|
590
634
|
|
|
591
|
-
async getChecksums(
|
|
635
|
+
async getChecksums(
|
|
636
|
+
checkpoint: utils.InternalOpId,
|
|
637
|
+
buckets: storage.BucketChecksumRequest[]
|
|
638
|
+
): Promise<utils.ChecksumMap> {
|
|
592
639
|
return this.checksumCache.getChecksumMap(checkpoint, buckets);
|
|
593
640
|
}
|
|
594
641
|
|
|
@@ -626,7 +673,7 @@ export class PostgresSyncRulesStorage
|
|
|
626
673
|
.first();
|
|
627
674
|
|
|
628
675
|
if (syncRulesRow == null) {
|
|
629
|
-
throw new Error('Cannot find
|
|
676
|
+
throw new Error('Cannot find replication stream status');
|
|
630
677
|
}
|
|
631
678
|
|
|
632
679
|
return {
|
|
@@ -662,11 +709,7 @@ export class PostgresSyncRulesStorage
|
|
|
662
709
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
663
710
|
`.execute();
|
|
664
711
|
|
|
665
|
-
await this.db.
|
|
666
|
-
DELETE FROM current_data
|
|
667
|
-
WHERE
|
|
668
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
669
|
-
`.execute();
|
|
712
|
+
await this.currentDataStore.deleteGroupRows(this.db, { groupId: this.group_id });
|
|
670
713
|
|
|
671
714
|
await this.db.sql`
|
|
672
715
|
DELETE FROM source_tables
|
|
@@ -823,7 +866,7 @@ export class PostgresSyncRulesStorage
|
|
|
823
866
|
|
|
824
867
|
if (doc == null) {
|
|
825
868
|
// Abort the connections - clients will have to retry later.
|
|
826
|
-
throw new framework.ServiceError(framework.ErrorCode.PSYNC_S2302, 'No active
|
|
869
|
+
throw new framework.ServiceError(framework.ErrorCode.PSYNC_S2302, 'No active replication stream available');
|
|
827
870
|
}
|
|
828
871
|
|
|
829
872
|
const sink = new LastValueSink<string>(undefined);
|
|
@@ -850,7 +893,7 @@ export class PostgresSyncRulesStorage
|
|
|
850
893
|
continue;
|
|
851
894
|
}
|
|
852
895
|
if (Number(notification.active_checkpoint.id) != doc.id) {
|
|
853
|
-
// Active
|
|
896
|
+
// Active replication stream changed - abort and restart the stream
|
|
854
897
|
break;
|
|
855
898
|
}
|
|
856
899
|
|
|
@@ -880,7 +923,7 @@ class PostgresReplicationCheckpoint implements storage.ReplicationCheckpoint {
|
|
|
880
923
|
public readonly lsn: string | null
|
|
881
924
|
) {}
|
|
882
925
|
|
|
883
|
-
getParameterSets(lookups: sync_rules.ScopedParameterLookup[]): Promise<sync_rules.SqliteJsonRow[]> {
|
|
884
|
-
return this.storage.getParameterSets(this, lookups);
|
|
926
|
+
getParameterSets(lookups: sync_rules.ScopedParameterLookup[], limit: number): Promise<sync_rules.SqliteJsonRow[]> {
|
|
927
|
+
return this.storage.getParameterSets(this, lookups, limit);
|
|
885
928
|
}
|
|
886
929
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { storage, utils } from '@powersync/service-core';
|
|
7
7
|
import { RequiredOperationBatchLimits } from '../../types/types.js';
|
|
8
|
+
import { postgresTableId } from '../table-id.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Batch of input operations.
|
|
@@ -89,13 +90,13 @@ export class RecordOperation {
|
|
|
89
90
|
/**
|
|
90
91
|
* In-memory cache key - must not be persisted.
|
|
91
92
|
*/
|
|
92
|
-
export function cacheKey(sourceTableId:
|
|
93
|
+
export function cacheKey(sourceTableId: storage.SourceTableId, id: storage.ReplicaId) {
|
|
93
94
|
return encodedCacheKey(sourceTableId, storage.serializeReplicaId(id));
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
98
|
* Calculates a cache key for a stored ReplicaId. This is usually stored as a bytea/Buffer.
|
|
98
99
|
*/
|
|
99
|
-
export function encodedCacheKey(sourceTableId:
|
|
100
|
-
return `${sourceTableId}.${storedKey.toString('base64')}`;
|
|
100
|
+
export function encodedCacheKey(sourceTableId: storage.SourceTableId, storedKey: Buffer) {
|
|
101
|
+
return `${postgresTableId(sourceTableId)}.${storedKey.toString('base64')}`;
|
|
101
102
|
}
|