@powersync/service-module-postgres-storage 0.11.2 → 0.12.0
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 +38 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/migrations/scripts/1771232439485-storage-version.d.ts +3 -0
- package/dist/@types/migrations/scripts/1771491856000-sync-plan.d.ts +3 -0
- package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +2 -10
- package/dist/@types/storage/PostgresCompactor.d.ts +2 -1
- package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +1 -10
- package/dist/@types/types/models/SyncRules.d.ts +12 -2
- package/dist/@types/types/models/json.d.ts +11 -0
- package/dist/@types/types/types.d.ts +2 -0
- package/dist/@types/utils/db.d.ts +9 -0
- package/dist/migrations/scripts/1771232439485-storage-version.js +111 -0
- package/dist/migrations/scripts/1771232439485-storage-version.js.map +1 -0
- package/dist/migrations/scripts/1771491856000-sync-plan.js +91 -0
- package/dist/migrations/scripts/1771491856000-sync-plan.js.map +1 -0
- package/dist/storage/PostgresBucketStorageFactory.js +16 -55
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.js +41 -60
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +14 -30
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -1
- package/dist/types/models/SyncRules.js +12 -1
- package/dist/types/models/SyncRules.js.map +1 -1
- package/dist/types/models/json.js +21 -0
- package/dist/types/models/json.js.map +1 -0
- package/dist/utils/db.js +32 -0
- package/dist/utils/db.js.map +1 -1
- package/dist/utils/test-utils.js +39 -10
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +8 -8
- package/src/migrations/scripts/1771232439485-storage-version.ts +44 -0
- package/src/migrations/scripts/1771491856000-sync-plan.ts +21 -0
- package/src/storage/PostgresBucketStorageFactory.ts +18 -65
- package/src/storage/PostgresCompactor.ts +46 -64
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +13 -33
- package/src/types/models/SyncRules.ts +16 -1
- package/src/types/models/json.ts +26 -0
- package/src/utils/db.ts +37 -0
- package/src/utils/test-utils.ts +30 -10
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1116 -21
- package/test/src/migrations.test.ts +8 -1
- package/test/src/storage.test.ts +11 -11
- package/test/src/storage_compacting.test.ts +51 -2
- package/test/src/storage_sync.test.ts +146 -4
- package/test/src/util.ts +3 -0
- package/test/src/__snapshots__/storage.test.ts.snap +0 -9
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { GetIntanceOptions, storage, SyncRulesBucketStorage, UpdateSyncRulesOptions } from '@powersync/service-core';
|
|
1
|
+
import { GetIntanceOptions, storage, SyncRulesBucketStorage } from '@powersync/service-core';
|
|
3
2
|
import * as pg_wire from '@powersync/service-jpgwire';
|
|
4
|
-
import * as sync_rules from '@powersync/service-sync-rules';
|
|
5
3
|
import crypto from 'crypto';
|
|
6
4
|
import * as uuid from 'uuid';
|
|
7
5
|
|
|
@@ -19,10 +17,7 @@ export type PostgresBucketStorageOptions = {
|
|
|
19
17
|
slot_name_prefix: string;
|
|
20
18
|
};
|
|
21
19
|
|
|
22
|
-
export class PostgresBucketStorageFactory
|
|
23
|
-
extends framework.BaseObserver<storage.BucketStorageFactoryListener>
|
|
24
|
-
implements storage.BucketStorageFactory
|
|
25
|
-
{
|
|
20
|
+
export class PostgresBucketStorageFactory extends storage.BucketStorageFactory {
|
|
26
21
|
readonly db: lib_postgres.DatabaseClient;
|
|
27
22
|
public readonly slot_name_prefix: string;
|
|
28
23
|
|
|
@@ -145,42 +140,8 @@ export class PostgresBucketStorageFactory
|
|
|
145
140
|
};
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
// TODO possibly share implementation in abstract class
|
|
149
|
-
async configureSyncRules(options: UpdateSyncRulesOptions): Promise<{
|
|
150
|
-
updated: boolean;
|
|
151
|
-
persisted_sync_rules?: storage.PersistedSyncRulesContent;
|
|
152
|
-
lock?: storage.ReplicationLock;
|
|
153
|
-
}> {
|
|
154
|
-
const next = await this.getNextSyncRulesContent();
|
|
155
|
-
const active = await this.getActiveSyncRulesContent();
|
|
156
|
-
|
|
157
|
-
if (next?.sync_rules_content == options.content) {
|
|
158
|
-
framework.logger.info('Sync rules from configuration unchanged');
|
|
159
|
-
return { updated: false };
|
|
160
|
-
} else if (next == null && active?.sync_rules_content == options.content) {
|
|
161
|
-
framework.logger.info('Sync rules from configuration unchanged');
|
|
162
|
-
return { updated: false };
|
|
163
|
-
} else {
|
|
164
|
-
framework.logger.info('Sync rules updated from configuration');
|
|
165
|
-
const persisted_sync_rules = await this.updateSyncRules(options);
|
|
166
|
-
return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
143
|
async updateSyncRules(options: storage.UpdateSyncRulesOptions): Promise<PostgresPersistedSyncRulesContent> {
|
|
171
|
-
|
|
172
|
-
if (options.validate) {
|
|
173
|
-
// Parse and validate before applying any changes
|
|
174
|
-
sync_rules.SqlSyncRules.fromYaml(options.content, {
|
|
175
|
-
// No schema-based validation at this point
|
|
176
|
-
schema: undefined,
|
|
177
|
-
defaultSchema: 'not_applicable', // Not needed for validation
|
|
178
|
-
throwOnError: true
|
|
179
|
-
});
|
|
180
|
-
} else {
|
|
181
|
-
// Apply unconditionally. Any errors will be reported via the diagnostics API.
|
|
182
|
-
}
|
|
183
|
-
|
|
144
|
+
const storageVersion = options.storageVersion ?? storage.CURRENT_STORAGE_VERSION;
|
|
184
145
|
return this.db.transaction(async (db) => {
|
|
185
146
|
await db.sql`
|
|
186
147
|
UPDATE sync_rules
|
|
@@ -197,7 +158,14 @@ export class PostgresBucketStorageFactory
|
|
|
197
158
|
nextval('sync_rules_id_sequence') AS id
|
|
198
159
|
)
|
|
199
160
|
INSERT INTO
|
|
200
|
-
sync_rules (
|
|
161
|
+
sync_rules (
|
|
162
|
+
id,
|
|
163
|
+
content,
|
|
164
|
+
sync_plan,
|
|
165
|
+
state,
|
|
166
|
+
slot_name,
|
|
167
|
+
storage_version
|
|
168
|
+
)
|
|
201
169
|
VALUES
|
|
202
170
|
(
|
|
203
171
|
(
|
|
@@ -206,7 +174,8 @@ export class PostgresBucketStorageFactory
|
|
|
206
174
|
FROM
|
|
207
175
|
next_id
|
|
208
176
|
),
|
|
209
|
-
${{ type: 'varchar', value: options.
|
|
177
|
+
${{ type: 'varchar', value: options.config.yaml }},
|
|
178
|
+
${{ type: 'json', value: options.config.plan }},
|
|
210
179
|
${{ type: 'varchar', value: storage.SyncRuleState.PROCESSING }},
|
|
211
180
|
CONCAT(
|
|
212
181
|
${{ type: 'varchar', value: this.slot_name_prefix }},
|
|
@@ -218,7 +187,8 @@ export class PostgresBucketStorageFactory
|
|
|
218
187
|
),
|
|
219
188
|
'_',
|
|
220
189
|
${{ type: 'varchar', value: crypto.randomBytes(2).toString('hex') }}
|
|
221
|
-
)
|
|
190
|
+
),
|
|
191
|
+
${{ type: 'int4', value: storageVersion }}
|
|
222
192
|
)
|
|
223
193
|
RETURNING
|
|
224
194
|
*
|
|
@@ -240,10 +210,8 @@ export class PostgresBucketStorageFactory
|
|
|
240
210
|
// The current one will continue serving sync requests until the next one has finished processing.
|
|
241
211
|
if (next != null && next.id == sync_rules_group_id) {
|
|
242
212
|
// We need to redo the "next" sync rules
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
validate: false
|
|
246
|
-
});
|
|
213
|
+
|
|
214
|
+
await this.updateSyncRules(next.asUpdateOptions());
|
|
247
215
|
// Pro-actively stop replicating
|
|
248
216
|
await this.db.sql`
|
|
249
217
|
UPDATE sync_rules
|
|
@@ -255,10 +223,7 @@ export class PostgresBucketStorageFactory
|
|
|
255
223
|
`.execute();
|
|
256
224
|
} else if (next == null && active?.id == sync_rules_group_id) {
|
|
257
225
|
// Slot removed for "active" sync rules, while there is no "next" one.
|
|
258
|
-
await this.updateSyncRules(
|
|
259
|
-
content: active.sync_rules_content,
|
|
260
|
-
validate: false
|
|
261
|
-
});
|
|
226
|
+
await this.updateSyncRules(active.asUpdateOptions());
|
|
262
227
|
|
|
263
228
|
// Pro-actively stop replicating, but still serve clients with existing data
|
|
264
229
|
await this.db.sql`
|
|
@@ -284,12 +249,6 @@ export class PostgresBucketStorageFactory
|
|
|
284
249
|
}
|
|
285
250
|
}
|
|
286
251
|
|
|
287
|
-
// TODO possibly share via abstract class
|
|
288
|
-
async getActiveSyncRules(options: storage.ParseSyncRulesOptions): Promise<storage.PersistedSyncRules | null> {
|
|
289
|
-
const content = await this.getActiveSyncRulesContent();
|
|
290
|
-
return content?.parsed(options) ?? null;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
252
|
async getActiveSyncRulesContent(): Promise<storage.PersistedSyncRulesContent | null> {
|
|
294
253
|
const activeRow = await this.db.sql`
|
|
295
254
|
SELECT
|
|
@@ -313,12 +272,6 @@ export class PostgresBucketStorageFactory
|
|
|
313
272
|
return new PostgresPersistedSyncRulesContent(this.db, activeRow);
|
|
314
273
|
}
|
|
315
274
|
|
|
316
|
-
// TODO possibly share via abstract class
|
|
317
|
-
async getNextSyncRules(options: storage.ParseSyncRulesOptions): Promise<storage.PersistedSyncRules | null> {
|
|
318
|
-
const content = await this.getNextSyncRulesContent();
|
|
319
|
-
return content?.parsed(options) ?? null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
275
|
async getNextSyncRulesContent(): Promise<storage.PersistedSyncRulesContent | null> {
|
|
323
276
|
const nextRow = await this.db.sql`
|
|
324
277
|
SELECT
|
|
@@ -75,37 +75,54 @@ export class PostgresCompactor {
|
|
|
75
75
|
async compact() {
|
|
76
76
|
if (this.buckets) {
|
|
77
77
|
for (let bucket of this.buckets) {
|
|
78
|
-
|
|
79
|
-
// through the buckets in a single query.
|
|
80
|
-
// That makes batching more tricky, so we leave for later.
|
|
81
|
-
await this.compactInternal(bucket);
|
|
78
|
+
await this.compactSingleBucket(bucket);
|
|
82
79
|
}
|
|
83
80
|
} else {
|
|
84
|
-
await this.
|
|
81
|
+
await this.compactAllBuckets();
|
|
85
82
|
}
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
async
|
|
89
|
-
const
|
|
85
|
+
private async compactAllBuckets() {
|
|
86
|
+
const DISCOVERY_BATCH_SIZE = 200;
|
|
87
|
+
let lastBucket = '';
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
const bucketRows = (await this.db.sql`
|
|
91
|
+
SELECT DISTINCT
|
|
92
|
+
bucket_name
|
|
93
|
+
FROM
|
|
94
|
+
bucket_data
|
|
95
|
+
WHERE
|
|
96
|
+
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
97
|
+
AND bucket_name > ${{ type: 'varchar', value: lastBucket }}
|
|
98
|
+
ORDER BY
|
|
99
|
+
bucket_name ASC
|
|
100
|
+
LIMIT
|
|
101
|
+
${{ type: 'int4', value: DISCOVERY_BATCH_SIZE }}
|
|
102
|
+
`.rows()) as { bucket_name: string }[];
|
|
103
|
+
|
|
104
|
+
if (bucketRows.length === 0) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const row of bucketRows) {
|
|
109
|
+
await this.compactSingleBucket(row.bucket_name);
|
|
110
|
+
}
|
|
90
111
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let bucketLower: string | null = null;
|
|
94
|
-
let bucketUpper: string | null = null;
|
|
95
|
-
const MAX_CHAR = String.fromCodePoint(0xffff);
|
|
96
|
-
|
|
97
|
-
if (bucket == null) {
|
|
98
|
-
bucketLower = '';
|
|
99
|
-
bucketUpper = MAX_CHAR;
|
|
100
|
-
} else if (bucket?.includes('[')) {
|
|
101
|
-
// Exact bucket name
|
|
102
|
-
bucketLower = bucket;
|
|
103
|
-
bucketUpper = bucket;
|
|
104
|
-
} else if (bucket) {
|
|
105
|
-
// Bucket definition name
|
|
106
|
-
bucketLower = `${bucket}[`;
|
|
107
|
-
bucketUpper = `${bucket}[${MAX_CHAR}`;
|
|
112
|
+
lastBucket = bucketRows[bucketRows.length - 1].bucket_name;
|
|
108
113
|
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async compactSingleBucket(bucket: string) {
|
|
117
|
+
const idLimitBytes = this.idLimitBytes;
|
|
118
|
+
|
|
119
|
+
let currentState: CurrentBucketState = {
|
|
120
|
+
bucket: bucket,
|
|
121
|
+
seen: new Map(),
|
|
122
|
+
trackingSize: 0,
|
|
123
|
+
lastNotPut: null,
|
|
124
|
+
opsSincePut: 0
|
|
125
|
+
};
|
|
109
126
|
|
|
110
127
|
let upperOpIdLimit = BIGINT_MAX;
|
|
111
128
|
|
|
@@ -123,16 +140,9 @@ export class PostgresCompactor {
|
|
|
123
140
|
bucket_data
|
|
124
141
|
WHERE
|
|
125
142
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
126
|
-
AND bucket_name
|
|
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
|
-
)
|
|
143
|
+
AND bucket_name = ${{ type: 'varchar', value: bucket }}
|
|
144
|
+
AND op_id < ${{ type: 'int8', value: upperOpIdLimit }}
|
|
134
145
|
ORDER BY
|
|
135
|
-
bucket_name DESC,
|
|
136
146
|
op_id DESC
|
|
137
147
|
LIMIT
|
|
138
148
|
${{ type: 'int4', value: this.moveBatchQueryLimit }}
|
|
@@ -150,32 +160,8 @@ export class PostgresCompactor {
|
|
|
150
160
|
// Set upperBound for the next batch
|
|
151
161
|
const lastBatchItem = batch[batch.length - 1];
|
|
152
162
|
upperOpIdLimit = lastBatchItem.op_id;
|
|
153
|
-
bucketUpper = lastBatchItem.bucket_name;
|
|
154
163
|
|
|
155
164
|
for (const doc of batch) {
|
|
156
|
-
if (currentState == null || doc.bucket_name != currentState.bucket) {
|
|
157
|
-
if (currentState != null && currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
158
|
-
// Important to flush before clearBucket()
|
|
159
|
-
await this.flush();
|
|
160
|
-
logger.info(
|
|
161
|
-
`Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
const bucket = currentState.bucket;
|
|
165
|
-
const clearOp = currentState.lastNotPut;
|
|
166
|
-
// Free memory before clearing bucket
|
|
167
|
-
currentState = null;
|
|
168
|
-
await this.clearBucket(bucket, clearOp);
|
|
169
|
-
}
|
|
170
|
-
currentState = {
|
|
171
|
-
bucket: doc.bucket_name,
|
|
172
|
-
seen: new Map(),
|
|
173
|
-
trackingSize: 0,
|
|
174
|
-
lastNotPut: null,
|
|
175
|
-
opsSincePut: 0
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
165
|
if (this.maxOpId != null && doc.op_id > this.maxOpId) {
|
|
180
166
|
continue;
|
|
181
167
|
}
|
|
@@ -237,16 +223,12 @@ export class PostgresCompactor {
|
|
|
237
223
|
}
|
|
238
224
|
|
|
239
225
|
await this.flush();
|
|
240
|
-
currentState
|
|
241
|
-
if (currentState
|
|
226
|
+
currentState.seen.clear();
|
|
227
|
+
if (currentState.lastNotPut != null && currentState.opsSincePut > 1) {
|
|
242
228
|
logger.info(
|
|
243
229
|
`Inserting CLEAR at ${this.group_id}:${currentState.bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
244
230
|
);
|
|
245
|
-
|
|
246
|
-
const clearOp = currentState.lastNotPut;
|
|
247
|
-
// Free memory before clearing bucket
|
|
248
|
-
currentState = null;
|
|
249
|
-
await this.clearBucket(bucket, clearOp);
|
|
231
|
+
await this.clearBucket(currentState.bucket, currentState.lastNotPut);
|
|
250
232
|
}
|
|
251
233
|
}
|
|
252
234
|
|
|
@@ -1,47 +1,27 @@
|
|
|
1
1
|
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
2
|
import { ErrorCode, logger, ServiceError } from '@powersync/lib-services-framework';
|
|
3
3
|
import { storage } from '@powersync/service-core';
|
|
4
|
-
import { SqlSyncRules, versionedHydrationState } from '@powersync/service-sync-rules';
|
|
5
|
-
|
|
6
4
|
import { models } from '../../types/types.js';
|
|
7
5
|
|
|
8
|
-
export class PostgresPersistedSyncRulesContent
|
|
9
|
-
public readonly slot_name: string;
|
|
10
|
-
|
|
11
|
-
public readonly id: number;
|
|
12
|
-
public readonly sync_rules_content: string;
|
|
13
|
-
public readonly last_checkpoint_lsn: string | null;
|
|
14
|
-
public readonly last_fatal_error: string | null;
|
|
15
|
-
public readonly last_keepalive_ts: Date | null;
|
|
16
|
-
public readonly last_checkpoint_ts: Date | null;
|
|
17
|
-
public readonly active: boolean;
|
|
6
|
+
export class PostgresPersistedSyncRulesContent extends storage.PersistedSyncRulesContent {
|
|
18
7
|
current_lock: storage.ReplicationLock | null = null;
|
|
19
8
|
|
|
20
9
|
constructor(
|
|
21
10
|
private db: lib_postgres.DatabaseClient,
|
|
22
11
|
row: models.SyncRulesDecoded
|
|
23
12
|
) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
id: this.id,
|
|
37
|
-
slot_name: this.slot_name,
|
|
38
|
-
sync_rules: SqlSyncRules.fromYaml(this.sync_rules_content, options),
|
|
39
|
-
hydratedSyncRules() {
|
|
40
|
-
return this.sync_rules.config.hydrate({
|
|
41
|
-
hydrationState: versionedHydrationState(this.id)
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
};
|
|
13
|
+
super({
|
|
14
|
+
id: Number(row.id),
|
|
15
|
+
sync_rules_content: row.content,
|
|
16
|
+
compiled_plan: row.sync_plan,
|
|
17
|
+
last_checkpoint_lsn: row.last_checkpoint_lsn,
|
|
18
|
+
slot_name: row.slot_name,
|
|
19
|
+
last_fatal_error: row.last_fatal_error,
|
|
20
|
+
last_checkpoint_ts: row.last_checkpoint_ts ? new Date(row.last_checkpoint_ts) : null,
|
|
21
|
+
last_keepalive_ts: row.last_keepalive_ts ? new Date(row.last_keepalive_ts) : null,
|
|
22
|
+
active: row.state == 'ACTIVE',
|
|
23
|
+
storageVersion: row.storage_version ?? storage.LEGACY_STORAGE_VERSION
|
|
24
|
+
});
|
|
45
25
|
}
|
|
46
26
|
|
|
47
27
|
async lock(): Promise<storage.ReplicationLock> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { framework, storage } from '@powersync/service-core';
|
|
2
2
|
import * as t from 'ts-codec';
|
|
3
3
|
import { bigint, pgwire_number } from '../codecs.js';
|
|
4
|
+
import { jsonContainerObject } from './json.js';
|
|
4
5
|
|
|
5
6
|
export const SyncRules = t.object({
|
|
6
7
|
id: pgwire_number,
|
|
@@ -47,7 +48,21 @@ export const SyncRules = t.object({
|
|
|
47
48
|
*/
|
|
48
49
|
last_fatal_error: t.Null.or(t.string),
|
|
49
50
|
keepalive_op: t.Null.or(bigint),
|
|
50
|
-
|
|
51
|
+
storage_version: t.Null.or(pgwire_number).optional(),
|
|
52
|
+
content: t.string,
|
|
53
|
+
sync_plan: t.Null.or(
|
|
54
|
+
jsonContainerObject(
|
|
55
|
+
t.object({
|
|
56
|
+
plan: t.any,
|
|
57
|
+
compatibility: t.object({
|
|
58
|
+
edition: t.number,
|
|
59
|
+
overrides: t.record(t.boolean),
|
|
60
|
+
maxTimeValuePrecision: t.number.optional()
|
|
61
|
+
}),
|
|
62
|
+
eventDescriptors: t.record(t.array(t.string))
|
|
63
|
+
})
|
|
64
|
+
)
|
|
65
|
+
)
|
|
51
66
|
});
|
|
52
67
|
|
|
53
68
|
export type SyncRules = t.Encoded<typeof SyncRules>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JsonContainer } from '@powersync/service-jsonbig';
|
|
2
|
+
import { Codec, codec } from 'ts-codec';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a codec to support {@link JsonContainer} values.
|
|
6
|
+
*
|
|
7
|
+
* Because our postgres client implementation wraps JSON objects in a {@link JsonContainer}, this intermediate layer is
|
|
8
|
+
* required to use JSON columns from Postgres in `ts-codec` models.
|
|
9
|
+
*
|
|
10
|
+
* Note that this serializes and deserializes values using {@link JSON}, so bigints are not supported.
|
|
11
|
+
*/
|
|
12
|
+
export function jsonContainerObject<I, O>(inner: Codec<I, O>): Codec<I, JsonContainer> {
|
|
13
|
+
return codec(
|
|
14
|
+
inner._tag,
|
|
15
|
+
(input) => {
|
|
16
|
+
return new JsonContainer(JSON.stringify(inner.encode(input)));
|
|
17
|
+
},
|
|
18
|
+
(json) => {
|
|
19
|
+
if (!(json instanceof JsonContainer)) {
|
|
20
|
+
throw new Error('Expected JsonContainer');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return inner.decode(JSON.parse(json.data));
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/utils/db.ts
CHANGED
|
@@ -9,6 +9,9 @@ export const NOTIFICATION_CHANNEL = 'powersynccheckpoints';
|
|
|
9
9
|
*/
|
|
10
10
|
export const sql = lib_postgres.sql;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Drop all Postgres storage tables used by the service, including migrations.
|
|
14
|
+
*/
|
|
12
15
|
export const dropTables = async (client: lib_postgres.DatabaseClient) => {
|
|
13
16
|
// Lock a connection for automatic schema search paths
|
|
14
17
|
await client.lockConnection(async (db) => {
|
|
@@ -23,5 +26,39 @@ export const dropTables = async (client: lib_postgres.DatabaseClient) => {
|
|
|
23
26
|
await db.sql`DROP TABLE IF EXISTS custom_write_checkpoints`.execute();
|
|
24
27
|
await db.sql`DROP SEQUENCE IF EXISTS op_id_sequence`.execute();
|
|
25
28
|
await db.sql`DROP SEQUENCE IF EXISTS sync_rules_id_sequence`.execute();
|
|
29
|
+
await db.sql`DROP TABLE IF EXISTS migrations`.execute();
|
|
26
30
|
});
|
|
27
31
|
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear all Postgres storage tables and reset sequences.
|
|
35
|
+
*
|
|
36
|
+
* Does not clear migration state.
|
|
37
|
+
*/
|
|
38
|
+
export const truncateTables = async (db: lib_postgres.DatabaseClient) => {
|
|
39
|
+
// Lock a connection for automatic schema search paths
|
|
40
|
+
await db.query(
|
|
41
|
+
{
|
|
42
|
+
statement: `TRUNCATE TABLE bucket_data,
|
|
43
|
+
bucket_parameters,
|
|
44
|
+
sync_rules,
|
|
45
|
+
instance,
|
|
46
|
+
current_data,
|
|
47
|
+
source_tables,
|
|
48
|
+
write_checkpoints,
|
|
49
|
+
custom_write_checkpoints,
|
|
50
|
+
connection_report_events RESTART IDENTITY CASCADE
|
|
51
|
+
`
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
statement: `ALTER SEQUENCE IF EXISTS op_id_sequence RESTART
|
|
55
|
+
WITH
|
|
56
|
+
1`
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
statement: `ALTER SEQUENCE IF EXISTS sync_rules_id_sequence RESTART
|
|
60
|
+
WITH
|
|
61
|
+
1`
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
};
|
package/src/utils/test-utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { PostgresMigrationAgent } from '../migrations/PostgresMigrationAgent.js'
|
|
|
3
3
|
import { normalizePostgresStorageConfig, PostgresStorageConfigDecoded } from '../types/types.js';
|
|
4
4
|
import { PostgresReportStorage } from '../storage/PostgresReportStorage.js';
|
|
5
5
|
import { PostgresBucketStorageFactory } from '../storage/PostgresBucketStorageFactory.js';
|
|
6
|
+
import { truncateTables } from './db.js';
|
|
6
7
|
|
|
7
8
|
export type PostgresTestStorageOptions = {
|
|
8
9
|
url: string;
|
|
@@ -22,7 +23,7 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
22
23
|
|
|
23
24
|
const TEST_CONNECTION_OPTIONS = normalizePostgresStorageConfig(BASE_CONFIG);
|
|
24
25
|
|
|
25
|
-
const
|
|
26
|
+
const runMigrations = async (options: { down: boolean; up: boolean }) => {
|
|
26
27
|
await using migrationManager: PowerSyncMigrationManager = new framework.MigrationManager();
|
|
27
28
|
await using migrationAgent = factoryOptions.migrationAgent
|
|
28
29
|
? factoryOptions.migrationAgent(BASE_CONFIG)
|
|
@@ -31,14 +32,16 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
31
32
|
|
|
32
33
|
const mockServiceContext = { configuration: { storage: BASE_CONFIG } } as unknown as ServiceContext;
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if (options.down) {
|
|
36
|
+
await migrationManager.migrate({
|
|
37
|
+
direction: framework.migrations.Direction.Down,
|
|
38
|
+
migrationContext: {
|
|
39
|
+
service_context: mockServiceContext
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
40
43
|
|
|
41
|
-
if (
|
|
44
|
+
if (options.up) {
|
|
42
45
|
await migrationManager.migrate({
|
|
43
46
|
direction: framework.migrations.Direction.Up,
|
|
44
47
|
migrationContext: {
|
|
@@ -48,11 +51,28 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
48
51
|
}
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
const migrate = async (direction: framework.migrations.Direction) => {
|
|
55
|
+
await runMigrations({
|
|
56
|
+
down: true,
|
|
57
|
+
up: direction == framework.migrations.Direction.Up
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const clearStorage = async () => {
|
|
62
|
+
await runMigrations({ down: false, up: true });
|
|
63
|
+
|
|
64
|
+
await using storageFactory = new PostgresBucketStorageFactory({
|
|
65
|
+
config: TEST_CONNECTION_OPTIONS,
|
|
66
|
+
slot_name_prefix: 'test_'
|
|
67
|
+
});
|
|
68
|
+
await truncateTables(storageFactory.db);
|
|
69
|
+
};
|
|
70
|
+
|
|
51
71
|
return {
|
|
52
72
|
reportFactory: async (options?: TestStorageOptions) => {
|
|
53
73
|
try {
|
|
54
74
|
if (!options?.doNotClear) {
|
|
55
|
-
await
|
|
75
|
+
await clearStorage();
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
return new PostgresReportStorage({
|
|
@@ -67,7 +87,7 @@ export function postgresTestSetup(factoryOptions: PostgresTestStorageOptions) {
|
|
|
67
87
|
factory: async (options?: TestStorageOptions) => {
|
|
68
88
|
try {
|
|
69
89
|
if (!options?.doNotClear) {
|
|
70
|
-
await
|
|
90
|
+
await clearStorage();
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
return new PostgresBucketStorageFactory({
|