@powersync/service-module-postgres-storage 0.4.0 → 0.4.2

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.
@@ -35,14 +35,7 @@ interface CurrentBucketState {
35
35
  /**
36
36
  * Additional options, primarily for testing.
37
37
  */
38
- export interface PostgresCompactOptions extends storage.CompactOptions {
39
- /** Minimum of 2 */
40
- clearBatchLimit?: number;
41
- /** Minimum of 1 */
42
- moveBatchLimit?: number;
43
- /** Minimum of 1 */
44
- moveBatchQueryLimit?: number;
45
- }
38
+ export interface PostgresCompactOptions extends storage.CompactOptions {}
46
39
 
47
40
  const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
48
41
  const DEFAULT_MOVE_BATCH_LIMIT = 2000;
@@ -99,15 +92,19 @@ export class PostgresCompactor {
99
92
 
100
93
  let bucketLower: string | null = null;
101
94
  let bucketUpper: string | null = null;
95
+ const MAX_CHAR = String.fromCodePoint(0xffff);
102
96
 
103
- if (bucket?.includes('[')) {
97
+ if (bucket == null) {
98
+ bucketLower = '';
99
+ bucketUpper = MAX_CHAR;
100
+ } else if (bucket?.includes('[')) {
104
101
  // Exact bucket name
105
102
  bucketLower = bucket;
106
103
  bucketUpper = bucket;
107
104
  } else if (bucket) {
108
105
  // Bucket definition name
109
106
  bucketLower = `${bucket}[`;
110
- bucketUpper = `${bucket}[\uFFFF`;
107
+ bucketUpper = `${bucket}[${MAX_CHAR}`;
111
108
  }
112
109
 
113
110
  let upperOpIdLimit = BIGINT_MAX;
@@ -126,10 +123,16 @@ export class PostgresCompactor {
126
123
  bucket_data
127
124
  WHERE
128
125
  group_id = ${{ type: 'int4', value: this.group_id }}
129
- AND bucket_name LIKE COALESCE(${{ type: 'varchar', value: bucketLower }}, '%')
130
- AND op_id < ${{ type: 'int8', value: upperOpIdLimit }}
126
+ AND bucket_name >= ${{ type: 'varchar', value: bucketLower }}
127
+ AND (
128
+ (
129
+ bucket_name = ${{ type: 'varchar', value: bucketUpper }}
130
+ AND op_id < ${{ type: 'int8', value: upperOpIdLimit }}
131
+ )
132
+ OR bucket_name < ${{ type: 'varchar', value: bucketUpper }} COLLATE "C" -- Use binary comparison
133
+ )
131
134
  ORDER BY
132
- bucket_name,
135
+ bucket_name DESC,
133
136
  op_id DESC
134
137
  LIMIT
135
138
  ${{ type: 'int4', value: this.moveBatchQueryLimit }}
@@ -145,7 +148,9 @@ export class PostgresCompactor {
145
148
  }
146
149
 
147
150
  // Set upperBound for the next batch
148
- upperOpIdLimit = batch[batch.length - 1].op_id;
151
+ const lastBatchItem = batch[batch.length - 1];
152
+ upperOpIdLimit = lastBatchItem.op_id;
153
+ bucketUpper = lastBatchItem.bucket_name;
149
154
 
150
155
  for (const doc of batch) {
151
156
  if (currentState == null || doc.bucket_name != currentState.bucket) {
@@ -21,13 +21,14 @@ import * as timers from 'timers/promises';
21
21
 
22
22
  import * as framework from '@powersync/lib-services-framework';
23
23
  import { StatementParam } from '@powersync/service-jpgwire';
24
- import { StoredRelationId } from '../types/models/SourceTable.js';
24
+ import { SourceTableDecoded, StoredRelationId } from '../types/models/SourceTable.js';
25
25
  import { pick } from '../utils/ts-codec.js';
26
26
  import { PostgresBucketBatch } from './batch/PostgresBucketBatch.js';
27
27
  import { PostgresWriteCheckpointAPI } from './checkpoints/PostgresWriteCheckpointAPI.js';
28
28
  import { PostgresBucketStorageFactory } from './PostgresBucketStorageFactory.js';
29
29
  import { PostgresCompactor } from './PostgresCompactor.js';
30
30
  import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
31
+ import { Decoded } from 'ts-codec';
31
32
 
32
33
  export type PostgresSyncRulesStorageOptions = {
33
34
  factory: PostgresBucketStorageFactory;
@@ -165,21 +166,39 @@ export class PostgresSyncRulesStorage
165
166
  type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
166
167
  }));
167
168
  return this.db.transaction(async (db) => {
168
- let sourceTableRow = await db.sql`
169
- SELECT
170
- *
171
- FROM
172
- source_tables
173
- WHERE
174
- group_id = ${{ type: 'int4', value: group_id }}
175
- AND connection_id = ${{ type: 'int4', value: connection_id }}
176
- AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
177
- AND schema_name = ${{ type: 'varchar', value: schema }}
178
- AND table_name = ${{ type: 'varchar', value: table }}
179
- AND replica_id_columns = ${{ type: 'jsonb', value: columns }}
180
- `
181
- .decoded(models.SourceTable)
182
- .first();
169
+ let sourceTableRow: SourceTableDecoded | null;
170
+ if (objectId != null) {
171
+ sourceTableRow = await db.sql`
172
+ SELECT
173
+ *
174
+ FROM
175
+ source_tables
176
+ WHERE
177
+ group_id = ${{ type: 'int4', value: group_id }}
178
+ AND connection_id = ${{ type: 'int4', value: connection_id }}
179
+ AND relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
180
+ AND schema_name = ${{ type: 'varchar', value: schema }}
181
+ AND table_name = ${{ type: 'varchar', value: table }}
182
+ AND replica_id_columns = ${{ type: 'jsonb', value: columns }}
183
+ `
184
+ .decoded(models.SourceTable)
185
+ .first();
186
+ } else {
187
+ sourceTableRow = await db.sql`
188
+ SELECT
189
+ *
190
+ FROM
191
+ source_tables
192
+ WHERE
193
+ group_id = ${{ type: 'int4', value: group_id }}
194
+ AND connection_id = ${{ type: 'int4', value: connection_id }}
195
+ AND schema_name = ${{ type: 'varchar', value: schema }}
196
+ AND table_name = ${{ type: 'varchar', value: table }}
197
+ AND replica_id_columns = ${{ type: 'jsonb', value: columns }}
198
+ `
199
+ .decoded(models.SourceTable)
200
+ .first();
201
+ }
183
202
 
184
203
  if (sourceTableRow == null) {
185
204
  const row = await db.sql`
@@ -198,7 +217,7 @@ export class PostgresSyncRulesStorage
198
217
  ${{ type: 'varchar', value: uuid.v4() }},
199
218
  ${{ type: 'int4', value: group_id }},
200
219
  ${{ type: 'int4', value: connection_id }},
201
- --- The objectId can be string | number, we store it as jsonb value
220
+ --- The objectId can be string | number | undefined, we store it as jsonb value
202
221
  ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
203
222
  ${{ type: 'varchar', value: schema }},
204
223
  ${{ type: 'varchar', value: table }},
@@ -225,25 +244,47 @@ export class PostgresSyncRulesStorage
225
244
  sourceTable.syncData = options.sync_rules.tableSyncsData(sourceTable);
226
245
  sourceTable.syncParameters = options.sync_rules.tableSyncsParameters(sourceTable);
227
246
 
228
- const truncatedTables = await db.sql`
229
- SELECT
230
- *
231
- FROM
232
- source_tables
233
- WHERE
234
- group_id = ${{ type: 'int4', value: group_id }}
235
- AND connection_id = ${{ type: 'int4', value: connection_id }}
236
- AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
237
- AND (
238
- relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
239
- OR (
247
+ let truncatedTables: SourceTableDecoded[] = [];
248
+ if (objectId != null) {
249
+ // relation_id present - check for renamed tables
250
+ truncatedTables = await db.sql`
251
+ SELECT
252
+ *
253
+ FROM
254
+ source_tables
255
+ WHERE
256
+ group_id = ${{ type: 'int4', value: group_id }}
257
+ AND connection_id = ${{ type: 'int4', value: connection_id }}
258
+ AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
259
+ AND (
260
+ relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
261
+ OR (
262
+ schema_name = ${{ type: 'varchar', value: schema }}
263
+ AND table_name = ${{ type: 'varchar', value: table }}
264
+ )
265
+ )
266
+ `
267
+ .decoded(models.SourceTable)
268
+ .rows();
269
+ } else {
270
+ // relation_id not present - only check for changed replica_id_columns
271
+ truncatedTables = await db.sql`
272
+ SELECT
273
+ *
274
+ FROM
275
+ source_tables
276
+ WHERE
277
+ group_id = ${{ type: 'int4', value: group_id }}
278
+ AND connection_id = ${{ type: 'int4', value: connection_id }}
279
+ AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
280
+ AND (
240
281
  schema_name = ${{ type: 'varchar', value: schema }}
241
282
  AND table_name = ${{ type: 'varchar', value: table }}
242
283
  )
243
- )
244
- `
245
- .decoded(models.SourceTable)
246
- .rows();
284
+ `
285
+ .decoded(models.SourceTable)
286
+ .rows();
287
+ }
247
288
 
248
289
  return {
249
290
  table: sourceTable,
@@ -616,7 +657,10 @@ export class PostgresSyncRulesStorage
616
657
  SET
617
658
  state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
618
659
  WHERE
619
- state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
660
+ (
661
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
662
+ OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
663
+ )
620
664
  AND id != ${{ type: 'int4', value: this.group_id }}
621
665
  `.execute();
622
666
  });
@@ -688,6 +732,7 @@ export class PostgresSyncRulesStorage
688
732
  sync_rules
689
733
  WHERE
690
734
  state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
735
+ OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
691
736
  ORDER BY
692
737
  id DESC
693
738
  LIMIT
@@ -750,7 +795,8 @@ export class PostgresSyncRulesStorage
750
795
  FROM
751
796
  sync_rules
752
797
  WHERE
753
- state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
798
+ state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
799
+ OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
754
800
  LIMIT
755
801
  1
756
802
  `
@@ -2,7 +2,7 @@ import * as t from 'ts-codec';
2
2
  import { bigint, jsonb, jsonb_raw, pgwire_number } from '../codecs.js';
3
3
 
4
4
  export type StoredRelationId = {
5
- object_id: string | number;
5
+ object_id: string | number | undefined;
6
6
  };
7
7
 
8
8
  export const ColumnDescriptor = t.object({
@@ -127,6 +127,62 @@ exports[`sync - postgres > expiring token 2`] = `
127
127
  ]
128
128
  `;
129
129
 
130
+ exports[`sync - postgres > sends checkpoint complete line for empty checkpoint 1`] = `
131
+ [
132
+ {
133
+ "checkpoint": {
134
+ "buckets": [
135
+ {
136
+ "bucket": "mybucket[]",
137
+ "checksum": -1221282404,
138
+ "count": 1,
139
+ "priority": 3,
140
+ },
141
+ ],
142
+ "last_op_id": "1",
143
+ "write_checkpoint": undefined,
144
+ },
145
+ },
146
+ {
147
+ "data": {
148
+ "after": "0",
149
+ "bucket": "mybucket[]",
150
+ "data": [
151
+ {
152
+ "checksum": 3073684892,
153
+ "data": "{"id":"t1","description":"sync"}",
154
+ "object_id": "t1",
155
+ "object_type": "test",
156
+ "op": "PUT",
157
+ "op_id": "1",
158
+ "subkey": "02d285ac-4f96-5124-8fba-c6d1df992dd1",
159
+ },
160
+ ],
161
+ "has_more": false,
162
+ "next_after": "1",
163
+ },
164
+ },
165
+ {
166
+ "checkpoint_complete": {
167
+ "last_op_id": "1",
168
+ },
169
+ },
170
+ {
171
+ "checkpoint_diff": {
172
+ "last_op_id": "1",
173
+ "removed_buckets": [],
174
+ "updated_buckets": [],
175
+ "write_checkpoint": "1",
176
+ },
177
+ },
178
+ {
179
+ "checkpoint_complete": {
180
+ "last_op_id": "1",
181
+ },
182
+ },
183
+ ]
184
+ `;
185
+
130
186
  exports[`sync - postgres > sync buckets in order 1`] = `
131
187
  [
132
188
  {
@@ -2,4 +2,4 @@ import { register } from '@powersync/service-core-tests';
2
2
  import { describe } from 'vitest';
3
3
  import { POSTGRES_STORAGE_FACTORY } from './util.js';
4
4
 
5
- describe('Postgres Sync Bucket Storage Compact', () => register.registerCompactTests(POSTGRES_STORAGE_FACTORY, {}));
5
+ describe('Postgres Sync Bucket Storage Compact', () => register.registerCompactTests(POSTGRES_STORAGE_FACTORY));