@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +125 -7
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/migrations/scripts/1771424826685-current-data-pending-deletes.d.ts +3 -0
  4. package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +6 -1
  5. package/dist/@types/storage/PostgresCompactor.d.ts +8 -2
  6. package/dist/@types/storage/PostgresReportStorage.d.ts +1 -1
  7. package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +14 -6
  8. package/dist/@types/storage/batch/OperationBatch.d.ts +2 -2
  9. package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +14 -10
  10. package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +17 -5
  11. package/dist/@types/storage/current-data-store.d.ts +85 -0
  12. package/dist/@types/storage/table-id.d.ts +2 -0
  13. package/dist/@types/types/models/CurrentData.d.ts +18 -3
  14. package/dist/@types/types/models/SyncRules.d.ts +9 -0
  15. package/dist/@types/types/models/models-index.d.ts +1 -1
  16. package/dist/@types/utils/bson.d.ts +1 -1
  17. package/dist/@types/utils/test-utils.d.ts +3 -3
  18. package/dist/@types/utils/utils-index.d.ts +1 -1
  19. package/dist/migrations/migration-utils.js +2 -2
  20. package/dist/migrations/migration-utils.js.map +1 -1
  21. package/dist/migrations/scripts/1684951997326-init.js +24 -100
  22. package/dist/migrations/scripts/1684951997326-init.js.map +1 -1
  23. package/dist/migrations/scripts/1749024804042-snapshot-progress.js +12 -88
  24. package/dist/migrations/scripts/1749024804042-snapshot-progress.js.map +1 -1
  25. package/dist/migrations/scripts/1756282360128-connection-reporting.js +12 -88
  26. package/dist/migrations/scripts/1756282360128-connection-reporting.js.map +1 -1
  27. package/dist/migrations/scripts/1771232439485-storage-version.js +14 -90
  28. package/dist/migrations/scripts/1771232439485-storage-version.js.map +1 -1
  29. package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js +8 -0
  30. package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js.map +1 -0
  31. package/dist/migrations/scripts/1771491856000-sync-plan.js +6 -82
  32. package/dist/migrations/scripts/1771491856000-sync-plan.js.map +1 -1
  33. package/dist/storage/PostgresBucketStorageFactory.js +47 -9
  34. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  35. package/dist/storage/PostgresCompactor.js +14 -6
  36. package/dist/storage/PostgresCompactor.js.map +1 -1
  37. package/dist/storage/PostgresReportStorage.js +3 -3
  38. package/dist/storage/PostgresReportStorage.js.map +1 -1
  39. package/dist/storage/PostgresSyncRulesStorage.js +93 -60
  40. package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
  41. package/dist/storage/batch/OperationBatch.js +2 -1
  42. package/dist/storage/batch/OperationBatch.js.map +1 -1
  43. package/dist/storage/batch/PostgresBucketBatch.js +297 -215
  44. package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
  45. package/dist/storage/batch/PostgresPersistedBatch.js +86 -81
  46. package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -1
  47. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +1 -1
  48. package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
  49. package/dist/storage/current-data-store.js +270 -0
  50. package/dist/storage/current-data-store.js.map +1 -0
  51. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +3 -3
  52. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -1
  53. package/dist/storage/table-id.js +8 -0
  54. package/dist/storage/table-id.js.map +1 -0
  55. package/dist/types/models/CurrentData.js +11 -2
  56. package/dist/types/models/CurrentData.js.map +1 -1
  57. package/dist/types/models/SyncRules.js +3 -1
  58. package/dist/types/models/SyncRules.js.map +1 -1
  59. package/dist/types/models/models-index.js +1 -1
  60. package/dist/types/models/models-index.js.map +1 -1
  61. package/dist/utils/bson.js.map +1 -1
  62. package/dist/utils/db.js +9 -0
  63. package/dist/utils/db.js.map +1 -1
  64. package/dist/utils/test-utils.js +39 -108
  65. package/dist/utils/test-utils.js.map +1 -1
  66. package/dist/utils/utils-index.js +1 -1
  67. package/dist/utils/utils-index.js.map +1 -1
  68. package/package.json +12 -13
  69. package/src/migrations/migration-utils.ts +2 -2
  70. package/src/migrations/scripts/1771424826685-current-data-pending-deletes.ts +10 -0
  71. package/src/storage/PostgresBucketStorageFactory.ts +60 -10
  72. package/src/storage/PostgresCompactor.ts +17 -8
  73. package/src/storage/PostgresReportStorage.ts +5 -5
  74. package/src/storage/PostgresSyncRulesStorage.ts +108 -65
  75. package/src/storage/batch/OperationBatch.ts +4 -3
  76. package/src/storage/batch/PostgresBucketBatch.ts +318 -240
  77. package/src/storage/batch/PostgresPersistedBatch.ts +92 -84
  78. package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +3 -1
  79. package/src/storage/current-data-store.ts +326 -0
  80. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +3 -6
  81. package/src/storage/table-id.ts +9 -0
  82. package/src/types/models/CurrentData.ts +17 -4
  83. package/src/types/models/SyncRules.ts +3 -1
  84. package/src/types/models/models-index.ts +1 -1
  85. package/src/utils/bson.ts +1 -1
  86. package/src/utils/db.ts +10 -0
  87. package/src/utils/test-utils.ts +16 -9
  88. package/src/utils/utils-index.ts +1 -1
  89. package/test/src/__snapshots__/storage.test.ts.snap +151 -0
  90. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  91. package/test/src/__snapshots__/storage_sync.test.ts.snap +1393 -16
  92. package/test/src/connection-report-storage.test.ts +4 -4
  93. package/test/src/env.ts +1 -1
  94. package/test/src/migrations.test.ts +1 -1
  95. package/test/src/setup.ts +1 -1
  96. package/test/src/storage.test.ts +138 -131
  97. package/test/src/storage_compacting.test.ts +80 -11
  98. package/test/src/storage_sync.test.ts +57 -54
  99. package/test/src/util.ts +4 -4
  100. 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,cAAc,eAAe,CAAC;AAC9B,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC"}
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-20260225160713",
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": "^11.1.0",
32
- "@powersync/lib-service-postgres": "0.0.0-dev-20260225160713",
33
- "@powersync/lib-services-framework": "0.0.0-dev-20260225160713",
34
- "@powersync/service-core": "0.0.0-dev-20260225160713",
35
- "@powersync/service-types": "0.0.0-dev-20260225160713",
36
- "@powersync/service-jpgwire": "0.0.0-dev-20260225160713",
37
- "@powersync/service-jsonbig": "0.17.12",
38
- "@powersync/service-sync-rules": "0.0.0-dev-20260225160713"
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": "^5.7.3",
42
- "@powersync/service-core-tests": "0.0.0-dev-20260225160713"
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 ./lib && tsc -b --clean",
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
- pg_total_relation_size('current_data') AS current_size_bytes,
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<{ current_size_bytes: bigint; parameter_size_bytes: bigint; operations_size_bytes: bigint }>();
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!.current_size_bytes)
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 = options.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
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 sync rules instance.
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" sync rules
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" sync rules, while there is no "next" one.
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" sync rules - don't update any.
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 | undefined;
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?: PostgresCompactOptions
60
+ options: PostgresCompactOptions
61
61
  ) {
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;
67
- this.buckets = options?.compactBuckets;
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 { toInteger } from 'ix/util/tointeger.js';
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 rules, if present, had the same options.
104
- * Parse sync rules if the options are different or if there is no cached value.
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
- return new PostgresCompactor(this.db, this.group_id, options).compact();
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(options: PopulateChecksumCacheOptions): Promise<PopulateChecksumCacheResults> {
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 startBatch(
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 batch = new PostgresBucketBatch({
351
- logger: options.logger ?? framework.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?.(batch));
366
-
367
- await callback(batch);
368
- await batch.flush();
369
- if (batch.last_flushed_op != null) {
370
- return { flushed_op: batch.last_flushed_op };
371
- } else {
372
- return null;
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
- SELECT DISTINCT
382
- ON (lookup, source_table, source_key) lookup,
383
- source_table,
384
- source_key,
385
- id,
386
- bucket_parameters
387
- FROM
388
- bucket_parameters
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
- jsonb_array_elements(${{
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
- AND id <= ${{ type: 'int8', value: checkpoint.checkpoint }}
401
- ORDER BY
402
- lookup,
403
- source_table,
404
- source_key,
405
- id DESC
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 groupedParameters = rows.map((row) => {
411
- return JSONBig.parse(row.bucket_parameters) as sync_rules.SqliteJsonRow;
412
- });
413
- return groupedParameters.flat();
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: Map<string, InternalOpId>,
464
+ dataBuckets: storage.BucketDataRequest[],
419
465
  options?: storage.BucketDataBatchOptions
420
466
  ): AsyncIterable<storage.SyncBucketDataChunk> {
421
- if (dataBuckets.size == 0) {
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 = Array.from(dataBuckets.entries()).map(([name, start]) => ({
434
- bucket_name: name,
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 = dataBuckets.get(bucket_name);
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(checkpoint: utils.InternalOpId, buckets: string[]): Promise<utils.ChecksumMap> {
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 sync rules status');
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.sql`
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 sync rules available');
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 sync rules changed - abort and restart the stream
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: string, id: storage.ReplicaId) {
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: string, storedKey: Buffer) {
100
- return `${sourceTableId}.${storedKey.toString('base64')}`;
100
+ export function encodedCacheKey(sourceTableId: storage.SourceTableId, storedKey: Buffer) {
101
+ return `${postgresTableId(sourceTableId)}.${storedKey.toString('base64')}`;
101
102
  }