@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
@@ -2,9 +2,10 @@ import { beforeEach, describe, expect, it } from 'vitest';
2
2
 
3
3
  import { Direction } from '@powersync/lib-services-framework';
4
4
  import { register } from '@powersync/service-core-tests';
5
+ import { dropTables, PostgresBucketStorageFactory } from '../../src/index.js';
5
6
  import { PostgresMigrationAgent } from '../../src/migrations/PostgresMigrationAgent.js';
6
7
  import { env } from './env.js';
7
- import { POSTGRES_STORAGE_FACTORY, POSTGRES_STORAGE_SETUP } from './util.js';
8
+ import { POSTGRES_STORAGE_FACTORY, POSTGRES_STORAGE_SETUP, TEST_CONNECTION_OPTIONS } from './util.js';
8
9
 
9
10
  const MIGRATION_AGENT_FACTORY = () => {
10
11
  return new PostgresMigrationAgent({ type: 'postgresql', uri: env.PG_STORAGE_TEST_URL, sslmode: 'disable' });
@@ -15,6 +16,12 @@ describe('Migrations', () => {
15
16
  // The migration tests clear the migration store, without running the down migrations.
16
17
  // This ensures all the down migrations have been run before.
17
18
  const setup = POSTGRES_STORAGE_SETUP;
19
+ await using factory = new PostgresBucketStorageFactory({
20
+ config: TEST_CONNECTION_OPTIONS,
21
+ slot_name_prefix: 'test_'
22
+ });
23
+
24
+ await dropTables(factory.db);
18
25
  await setup.migrate(Direction.Down);
19
26
  });
20
27
 
@@ -1,5 +1,5 @@
1
- import { storage } from '@powersync/service-core';
2
- import { register, TEST_TABLE, test_utils } from '@powersync/service-core-tests';
1
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
2
+ import { bucketRequestMap, register, TEST_TABLE, test_utils } from '@powersync/service-core-tests';
3
3
  import { describe, expect, test } from 'vitest';
4
4
  import { POSTGRES_STORAGE_FACTORY } from './util.js';
5
5
 
@@ -24,16 +24,16 @@ describe('Postgres Sync Bucket Storage - pg-specific', () => {
24
24
  // Test syncing a batch of data that is small in count,
25
25
  // but large enough in size to be split over multiple returned chunks.
26
26
  // Similar to the above test, but splits over 1MB chunks.
27
- const sync_rules = test_utils.testRules(
28
- `
27
+ await using factory = await POSTGRES_STORAGE_FACTORY();
28
+ const syncRules = await factory.updateSyncRules(
29
+ updateSyncRulesFromYaml(`
29
30
  bucket_definitions:
30
31
  global:
31
32
  data:
32
33
  - SELECT id, description FROM "%"
33
- `
34
+ `)
34
35
  );
35
- await using factory = await POSTGRES_STORAGE_FACTORY();
36
- const bucketStorage = factory.getInstance(sync_rules);
36
+ const bucketStorage = factory.getInstance(syncRules);
37
37
 
38
38
  const result = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
39
39
  const sourceTable = TEST_TABLE;
@@ -87,7 +87,7 @@ describe('Postgres Sync Bucket Storage - pg-specific', () => {
87
87
  const options: storage.BucketDataBatchOptions = {};
88
88
 
89
89
  const batch1 = await test_utils.fromAsync(
90
- bucketStorage.getBucketDataBatch(checkpoint, new Map([['global[]', 0n]]), options)
90
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]), options)
91
91
  );
92
92
  expect(test_utils.getBatchData(batch1)).toEqual([
93
93
  { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
@@ -101,7 +101,7 @@ describe('Postgres Sync Bucket Storage - pg-specific', () => {
101
101
  const batch2 = await test_utils.fromAsync(
102
102
  bucketStorage.getBucketDataBatch(
103
103
  checkpoint,
104
- new Map([['global[]', BigInt(batch1[0].chunkData.next_after)]]),
104
+ bucketRequestMap(syncRules, [['global[]', BigInt(batch1[0].chunkData.next_after)]]),
105
105
  options
106
106
  )
107
107
  );
@@ -117,7 +117,7 @@ describe('Postgres Sync Bucket Storage - pg-specific', () => {
117
117
  const batch3 = await test_utils.fromAsync(
118
118
  bucketStorage.getBucketDataBatch(
119
119
  checkpoint,
120
- new Map([['global[]', BigInt(batch2[0].chunkData.next_after)]]),
120
+ bucketRequestMap(syncRules, [['global[]', BigInt(batch2[0].chunkData.next_after)]]),
121
121
  options
122
122
  )
123
123
  );
@@ -133,7 +133,7 @@ describe('Postgres Sync Bucket Storage - pg-specific', () => {
133
133
  const batch4 = await test_utils.fromAsync(
134
134
  bucketStorage.getBucketDataBatch(
135
135
  checkpoint,
136
- new Map([['global[]', BigInt(batch3[0].chunkData.next_after)]]),
136
+ bucketRequestMap(syncRules, [['global[]', BigInt(batch3[0].chunkData.next_after)]]),
137
137
  options
138
138
  )
139
139
  );
@@ -1,5 +1,54 @@
1
- import { register } from '@powersync/service-core-tests';
2
- import { describe } from 'vitest';
1
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
2
+ import { bucketRequest, bucketRequestMap, register, TEST_TABLE, test_utils } from '@powersync/service-core-tests';
3
+ import { describe, expect, test } from 'vitest';
3
4
  import { POSTGRES_STORAGE_FACTORY } from './util.js';
4
5
 
5
6
  describe('Postgres Sync Bucket Storage Compact', () => register.registerCompactTests(POSTGRES_STORAGE_FACTORY));
7
+
8
+ describe('Postgres Compact - explicit bucket name', () => {
9
+ test('compacts a specific bucket by exact name', async () => {
10
+ await using factory = await POSTGRES_STORAGE_FACTORY();
11
+ const syncRules = await factory.updateSyncRules(
12
+ updateSyncRulesFromYaml(`
13
+ bucket_definitions:
14
+ global:
15
+ data: [select * from test]
16
+ `)
17
+ );
18
+ const bucketStorage = factory.getInstance(syncRules);
19
+
20
+ const result = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
21
+ await batch.save({
22
+ sourceTable: TEST_TABLE,
23
+ tag: storage.SaveOperationTag.INSERT,
24
+ after: { id: 't1' },
25
+ afterReplicaId: test_utils.rid('t1')
26
+ });
27
+ await batch.save({
28
+ sourceTable: TEST_TABLE,
29
+ tag: storage.SaveOperationTag.UPDATE,
30
+ after: { id: 't1' },
31
+ afterReplicaId: test_utils.rid('t1')
32
+ });
33
+ await batch.commit('1/1');
34
+ });
35
+
36
+ const checkpoint = result!.flushed_op;
37
+
38
+ // Compact with an explicit bucket name — exercises the this.buckets
39
+ // iteration path, NOT the compactAllBuckets discovery path.
40
+ await bucketStorage.compact({
41
+ compactBuckets: [bucketRequest(syncRules, 'global[]')],
42
+ minBucketChanges: 1
43
+ });
44
+
45
+ const batch = await test_utils.oneFromAsync(
46
+ bucketStorage.getBucketDataBatch(checkpoint, bucketRequestMap(syncRules, [['global[]', 0n]]))
47
+ );
48
+
49
+ expect(batch.chunkData.data).toMatchObject([
50
+ { op_id: '1', op: 'MOVE' },
51
+ { op_id: '2', op: 'PUT', object_id: 't1' }
52
+ ]);
53
+ });
54
+ });
@@ -1,12 +1,154 @@
1
- import { register } from '@powersync/service-core-tests';
2
- import { describe } from 'vitest';
3
- import { POSTGRES_STORAGE_FACTORY } from './util.js';
1
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
2
+ import { bucketRequest, register, TEST_TABLE, test_utils } from '@powersync/service-core-tests';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { POSTGRES_STORAGE_FACTORY, TEST_STORAGE_VERSIONS } from './util.js';
4
5
 
5
6
  /**
6
7
  * Bucket compacting is not yet implemented.
7
8
  * This causes the internal compacting test to fail.
8
9
  * Other tests have been verified manually.
9
10
  */
11
+ function registerStorageVersionTests(storageVersion: number) {
12
+ describe(`storage v${storageVersion}`, () => {
13
+ const storageFactory = POSTGRES_STORAGE_FACTORY;
14
+
15
+ register.registerSyncTests(storageFactory, { storageVersion });
16
+
17
+ test('large batch (2)', async () => {
18
+ // Test syncing a batch of data that is small in count,
19
+ // but large enough in size to be split over multiple returned chunks.
20
+ // Similar to the above test, but splits over 1MB chunks.
21
+ await using factory = await storageFactory();
22
+ const syncRules = await factory.updateSyncRules(
23
+ updateSyncRulesFromYaml(
24
+ `
25
+ bucket_definitions:
26
+ global:
27
+ data:
28
+ - SELECT id, description FROM "%"
29
+ `,
30
+ { storageVersion }
31
+ )
32
+ );
33
+ const bucketStorage = factory.getInstance(syncRules);
34
+ const globalBucket = bucketRequest(syncRules, 'global[]');
35
+
36
+ const result = await bucketStorage.startBatch(test_utils.BATCH_OPTIONS, async (batch) => {
37
+ const sourceTable = TEST_TABLE;
38
+
39
+ const largeDescription = '0123456789'.repeat(2_000_00);
40
+
41
+ await batch.save({
42
+ sourceTable,
43
+ tag: storage.SaveOperationTag.INSERT,
44
+ after: {
45
+ id: 'test1',
46
+ description: 'test1'
47
+ },
48
+ afterReplicaId: test_utils.rid('test1')
49
+ });
50
+
51
+ await batch.save({
52
+ sourceTable,
53
+ tag: storage.SaveOperationTag.INSERT,
54
+ after: {
55
+ id: 'large1',
56
+ description: largeDescription
57
+ },
58
+ afterReplicaId: test_utils.rid('large1')
59
+ });
60
+
61
+ // Large enough to split the returned batch
62
+ await batch.save({
63
+ sourceTable,
64
+ tag: storage.SaveOperationTag.INSERT,
65
+ after: {
66
+ id: 'large2',
67
+ description: largeDescription
68
+ },
69
+ afterReplicaId: test_utils.rid('large2')
70
+ });
71
+
72
+ await batch.save({
73
+ sourceTable,
74
+ tag: storage.SaveOperationTag.INSERT,
75
+ after: {
76
+ id: 'test3',
77
+ description: 'test3'
78
+ },
79
+ afterReplicaId: test_utils.rid('test3')
80
+ });
81
+ });
82
+
83
+ const checkpoint = result!.flushed_op;
84
+
85
+ const options: storage.BucketDataBatchOptions = {};
86
+
87
+ const batch1 = await test_utils.fromAsync(
88
+ bucketStorage.getBucketDataBatch(checkpoint, new Map([[globalBucket, 0n]]), options)
89
+ );
90
+ expect(test_utils.getBatchData(batch1)).toEqual([
91
+ { op_id: '1', op: 'PUT', object_id: 'test1', checksum: 2871785649 }
92
+ ]);
93
+ expect(test_utils.getBatchMeta(batch1)).toEqual({
94
+ after: '0',
95
+ has_more: true,
96
+ next_after: '1'
97
+ });
98
+
99
+ const batch2 = await test_utils.fromAsync(
100
+ bucketStorage.getBucketDataBatch(
101
+ checkpoint,
102
+ new Map([[globalBucket, BigInt(batch1[0].chunkData.next_after)]]),
103
+ options
104
+ )
105
+ );
106
+ expect(test_utils.getBatchData(batch2)).toEqual([
107
+ { op_id: '2', op: 'PUT', object_id: 'large1', checksum: 1178768505 }
108
+ ]);
109
+ expect(test_utils.getBatchMeta(batch2)).toEqual({
110
+ after: '1',
111
+ has_more: true,
112
+ next_after: '2'
113
+ });
114
+
115
+ const batch3 = await test_utils.fromAsync(
116
+ bucketStorage.getBucketDataBatch(
117
+ checkpoint,
118
+ new Map([[globalBucket, BigInt(batch2[0].chunkData.next_after)]]),
119
+ options
120
+ )
121
+ );
122
+ expect(test_utils.getBatchData(batch3)).toEqual([
123
+ { op_id: '3', op: 'PUT', object_id: 'large2', checksum: 1607205872 }
124
+ ]);
125
+ expect(test_utils.getBatchMeta(batch3)).toEqual({
126
+ after: '2',
127
+ has_more: true,
128
+ next_after: '3'
129
+ });
130
+
131
+ const batch4 = await test_utils.fromAsync(
132
+ bucketStorage.getBucketDataBatch(
133
+ checkpoint,
134
+ new Map([[globalBucket, BigInt(batch3[0].chunkData.next_after)]]),
135
+ options
136
+ )
137
+ );
138
+ expect(test_utils.getBatchData(batch4)).toEqual([
139
+ { op_id: '4', op: 'PUT', object_id: 'test3', checksum: 1359888332 }
140
+ ]);
141
+ expect(test_utils.getBatchMeta(batch4)).toEqual({
142
+ after: '3',
143
+ has_more: false,
144
+ next_after: '4'
145
+ });
146
+ });
147
+ });
148
+ }
149
+
10
150
  describe('sync - postgres', () => {
11
- register.registerSyncTests(POSTGRES_STORAGE_FACTORY);
151
+ for (const storageVersion of TEST_STORAGE_VERSIONS) {
152
+ registerStorageVersionTests(storageVersion);
153
+ }
12
154
  });
package/test/src/util.ts CHANGED
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { normalizePostgresStorageConfig, PostgresMigrationAgent } from '../../src/index.js';
4
4
  import { env } from './env.js';
5
5
  import { postgresTestSetup } from '../../src/utils/test-utils.js';
6
+ import { CURRENT_STORAGE_VERSION, LEGACY_STORAGE_VERSION } from '@powersync/service-core';
6
7
 
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
@@ -34,3 +35,5 @@ export const POSTGRES_STORAGE_SETUP = postgresTestSetup({
34
35
 
35
36
  export const POSTGRES_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.factory;
36
37
  export const POSTGRES_REPORT_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.reportFactory;
38
+
39
+ export const TEST_STORAGE_VERSIONS = [LEGACY_STORAGE_VERSION, CURRENT_STORAGE_VERSION];
@@ -1,9 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`Postgres Sync Bucket Storage - Data > empty storage metrics 1`] = `
4
- {
5
- "operations_size_bytes": 16384,
6
- "parameters_size_bytes": 32768,
7
- "replication_size_bytes": 16384,
8
- }
9
- `;