@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/@types/migrations/scripts/1771232439485-storage-version.d.ts +3 -0
  4. package/dist/@types/migrations/scripts/1771491856000-sync-plan.d.ts +3 -0
  5. package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +2 -10
  6. package/dist/@types/storage/PostgresCompactor.d.ts +2 -1
  7. package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +1 -10
  8. package/dist/@types/types/models/SyncRules.d.ts +12 -2
  9. package/dist/@types/types/models/json.d.ts +11 -0
  10. package/dist/@types/types/types.d.ts +2 -0
  11. package/dist/@types/utils/db.d.ts +9 -0
  12. package/dist/migrations/scripts/1771232439485-storage-version.js +111 -0
  13. package/dist/migrations/scripts/1771232439485-storage-version.js.map +1 -0
  14. package/dist/migrations/scripts/1771491856000-sync-plan.js +91 -0
  15. package/dist/migrations/scripts/1771491856000-sync-plan.js.map +1 -0
  16. package/dist/storage/PostgresBucketStorageFactory.js +16 -55
  17. package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
  18. package/dist/storage/PostgresCompactor.js +41 -60
  19. package/dist/storage/PostgresCompactor.js.map +1 -1
  20. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +14 -30
  21. package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -1
  22. package/dist/types/models/SyncRules.js +12 -1
  23. package/dist/types/models/SyncRules.js.map +1 -1
  24. package/dist/types/models/json.js +21 -0
  25. package/dist/types/models/json.js.map +1 -0
  26. package/dist/utils/db.js +32 -0
  27. package/dist/utils/db.js.map +1 -1
  28. package/dist/utils/test-utils.js +39 -10
  29. package/dist/utils/test-utils.js.map +1 -1
  30. package/package.json +8 -8
  31. package/src/migrations/scripts/1771232439485-storage-version.ts +44 -0
  32. package/src/migrations/scripts/1771491856000-sync-plan.ts +21 -0
  33. package/src/storage/PostgresBucketStorageFactory.ts +18 -65
  34. package/src/storage/PostgresCompactor.ts +46 -64
  35. package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +13 -33
  36. package/src/types/models/SyncRules.ts +16 -1
  37. package/src/types/models/json.ts +26 -0
  38. package/src/utils/db.ts +37 -0
  39. package/src/utils/test-utils.ts +30 -10
  40. package/test/src/__snapshots__/storage_sync.test.ts.snap +1116 -21
  41. package/test/src/migrations.test.ts +8 -1
  42. package/test/src/storage.test.ts +11 -11
  43. package/test/src/storage_compacting.test.ts +51 -2
  44. package/test/src/storage_sync.test.ts +146 -4
  45. package/test/src/util.ts +3 -0
  46. package/test/src/__snapshots__/storage.test.ts.snap +0 -9
@@ -1,7 +1,5 @@
1
- import * as framework from '@powersync/lib-services-framework';
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
- // TODO some shared implementation for this might be nice
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 (id, content, state, slot_name)
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.content }},
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
- await this.updateSyncRules({
244
- content: next.sync_rules_content,
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
- // We can make this more efficient later on by iterating
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.compactInternal(undefined);
81
+ await this.compactAllBuckets();
85
82
  }
86
83
  }
87
84
 
88
- async compactInternal(bucket: string | undefined) {
89
- const idLimitBytes = this.idLimitBytes;
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
- let currentState: CurrentBucketState | null = null;
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 >= ${{ 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
- )
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?.seen.clear();
241
- if (currentState?.lastNotPut != null && currentState?.opsSincePut > 1) {
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
- const bucket = currentState.bucket;
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 implements storage.PersistedSyncRulesContent {
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
- this.id = Number(row.id);
25
- this.sync_rules_content = row.content;
26
- this.last_checkpoint_lsn = row.last_checkpoint_lsn;
27
- this.slot_name = row.slot_name;
28
- this.last_fatal_error = row.last_fatal_error;
29
- this.last_checkpoint_ts = row.last_checkpoint_ts ? new Date(row.last_checkpoint_ts) : null;
30
- this.last_keepalive_ts = row.last_keepalive_ts ? new Date(row.last_keepalive_ts) : null;
31
- this.active = row.state == 'ACTIVE';
32
- }
33
-
34
- parsed(options: storage.ParseSyncRulesOptions): storage.PersistedSyncRules {
35
- return {
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
- content: t.string
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
+ };
@@ -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 migrate = async (direction: framework.migrations.Direction) => {
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
- await migrationManager.migrate({
35
- direction: framework.migrations.Direction.Down,
36
- migrationContext: {
37
- service_context: mockServiceContext
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 (direction == framework.migrations.Direction.Up) {
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 migrate(framework.migrations.Direction.Up);
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 migrate(framework.migrations.Direction.Up);
90
+ await clearStorage();
71
91
  }
72
92
 
73
93
  return new PostgresBucketStorageFactory({