@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.
- package/CHANGELOG.md +26 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +3 -3
- package/dist/@types/storage/PostgresCompactor.d.ts +0 -6
- package/dist/@types/types/models/SourceTable.d.ts +1 -1
- package/dist/storage/PostgresBucketStorageFactory.js +23 -8
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.js +19 -6
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.js +81 -34
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/package.json +5 -5
- package/src/storage/PostgresBucketStorageFactory.ts +27 -9
- package/src/storage/PostgresCompactor.ts +19 -14
- package/src/storage/PostgresSyncRulesStorage.ts +81 -35
- package/src/types/models/SourceTable.ts +1 -1
- package/test/src/__snapshots__/storage_sync.test.ts.snap +56 -0
- package/test/src/storage_compacting.test.ts +1 -1
|
@@ -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
|
|
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}[
|
|
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
|
|
130
|
-
AND
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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 = ${{
|
|
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
|
`
|
|
@@ -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));
|