@powersync/service-module-mongodb-storage 0.17.0 → 0.18.1

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 (185) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +34 -16
  3. package/dist/storage/MongoBucketStorage.js +219 -70
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +42 -23
  6. package/dist/storage/implementation/MongoBucketBatch.js +61 -44
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoChecksums.d.ts +10 -9
  9. package/dist/storage/implementation/MongoChecksums.js +3 -111
  10. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  11. package/dist/storage/implementation/MongoCompactor.d.ts +2 -0
  12. package/dist/storage/implementation/MongoCompactor.js +18 -9
  13. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
  15. package/dist/storage/implementation/MongoHydrationState.js +49 -0
  16. package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +6 -5
  18. package/dist/storage/implementation/MongoParameterCompactor.js +24 -1
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +13 -0
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +64 -0
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +123 -0
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
  29. package/dist/storage/implementation/MongoStorageProvider.js +5 -2
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +41 -23
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +51 -43
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +3 -3
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +6 -0
  38. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
  39. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
  40. package/dist/storage/implementation/common/PersistedBatch.d.ts +1 -2
  41. package/dist/storage/implementation/common/PersistedBatch.js +5 -0
  42. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  43. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
  44. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  45. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
  46. package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
  47. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
  48. package/dist/storage/implementation/db.js +9 -14
  49. package/dist/storage/implementation/db.js.map +1 -1
  50. package/dist/storage/implementation/models.d.ts +4 -13
  51. package/dist/storage/implementation/models.js.map +1 -1
  52. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +9 -1
  53. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +42 -26
  54. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  55. package/dist/storage/implementation/v1/MongoChecksumsV1.js +114 -3
  56. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
  57. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +15 -17
  58. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +47 -45
  59. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  60. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
  61. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
  62. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
  63. package/dist/storage/implementation/v1/models.d.ts +10 -1
  64. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -4
  65. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +286 -259
  66. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  67. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +27 -7
  68. package/dist/storage/implementation/v3/MongoChecksumsV3.js +202 -18
  69. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +19 -2
  71. package/dist/storage/implementation/v3/MongoCompactorV3.js +511 -22
  72. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +62 -0
  74. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +350 -0
  75. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
  76. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +40 -28
  77. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +232 -116
  78. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  79. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +1 -1
  80. package/dist/storage/implementation/v3/PersistedBatchV3.js +43 -20
  81. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +52 -16
  83. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
  85. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
  86. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
  87. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +13 -12
  88. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +29 -27
  89. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  90. package/dist/storage/implementation/v3/bucket-format.d.ts +4 -0
  91. package/dist/storage/implementation/v3/bucket-format.js +58 -0
  92. package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
  93. package/dist/storage/implementation/v3/chunking.d.ts +10 -0
  94. package/dist/storage/implementation/v3/chunking.js +28 -0
  95. package/dist/storage/implementation/v3/chunking.js.map +1 -0
  96. package/dist/storage/implementation/v3/models.d.ts +64 -21
  97. package/dist/storage/implementation/v3/models.js +11 -28
  98. package/dist/storage/implementation/v3/models.js.map +1 -1
  99. package/dist/storage/implementation/v3/source-table-utils.d.ts +55 -0
  100. package/dist/storage/implementation/v3/source-table-utils.js +214 -0
  101. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
  102. package/dist/storage/storage-index.d.ts +4 -4
  103. package/dist/storage/storage-index.js +4 -4
  104. package/dist/storage/storage-index.js.map +1 -1
  105. package/dist/types/types.d.ts +2 -2
  106. package/dist/utils/test-utils.d.ts +1 -2
  107. package/dist/utils/test-utils.js +5 -1
  108. package/dist/utils/test-utils.js.map +1 -1
  109. package/dist/utils/util.d.ts +1 -1
  110. package/dist/utils/util.js +3 -2
  111. package/dist/utils/util.js.map +1 -1
  112. package/package.json +9 -19
  113. package/src/storage/MongoBucketStorage.ts +334 -87
  114. package/src/storage/implementation/MongoBucketBatch.ts +86 -58
  115. package/src/storage/implementation/MongoChecksums.ts +6 -138
  116. package/src/storage/implementation/MongoCompactor.ts +19 -9
  117. package/src/storage/implementation/MongoHydrationState.ts +61 -0
  118. package/src/storage/implementation/MongoParameterCompactor.ts +24 -5
  119. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +88 -0
  120. package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
  121. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +154 -0
  122. package/src/storage/implementation/MongoStorageProvider.ts +5 -2
  123. package/src/storage/implementation/MongoSyncBucketStorage.ts +70 -56
  124. package/src/storage/implementation/MongoSyncRulesLock.ts +7 -4
  125. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +7 -0
  126. package/src/storage/implementation/common/PersistedBatch.ts +7 -3
  127. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
  128. package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
  129. package/src/storage/implementation/db.ts +11 -19
  130. package/src/storage/implementation/models.ts +4 -14
  131. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +55 -34
  132. package/src/storage/implementation/v1/MongoChecksumsV1.ts +137 -4
  133. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +70 -64
  134. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
  135. package/src/storage/implementation/v1/models.ts +11 -1
  136. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +358 -314
  137. package/src/storage/implementation/v3/MongoChecksumsV3.ts +262 -26
  138. package/src/storage/implementation/v3/MongoCompactorV3.ts +646 -29
  139. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +480 -0
  140. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +315 -157
  141. package/src/storage/implementation/v3/PersistedBatchV3.ts +62 -36
  142. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +59 -19
  143. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
  144. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +41 -36
  145. package/src/storage/implementation/v3/bucket-format.ts +70 -0
  146. package/src/storage/implementation/v3/chunking.ts +38 -0
  147. package/src/storage/implementation/v3/models.ts +93 -55
  148. package/src/storage/implementation/v3/source-table-utils.ts +392 -0
  149. package/src/storage/storage-index.ts +12 -4
  150. package/src/utils/test-utils.ts +6 -3
  151. package/src/utils/util.ts +3 -2
  152. package/test/src/__snapshots__/storage.test.ts.snap +0 -75
  153. package/test/src/__snapshots__/storage_sync.test.ts.snap +48 -48
  154. package/test/src/cleanup-stopped-sync-configs.test.ts +648 -0
  155. package/test/src/storage.test.ts +11 -11
  156. package/test/src/storage_compacting.test.ts +1956 -6
  157. package/test/src/storage_sync.test.ts +1077 -68
  158. package/test/src/storeCurrentData.test.ts +17 -7
  159. package/test/src/util.ts +2 -1
  160. package/tsconfig.json +1 -4
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
  163. package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
  164. package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
  165. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
  166. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
  167. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  168. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
  169. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
  170. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  171. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
  172. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
  173. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -1
  174. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +0 -9
  175. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +0 -18
  176. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +0 -1
  177. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +0 -4
  178. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
  179. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
  180. package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
  181. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
  182. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
  183. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
  184. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +0 -24
  185. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
@@ -0,0 +1,648 @@
1
+ import { MongoSyncBucketStorageV3 } from '@module/storage/implementation/v3/MongoSyncBucketStorageV3.js';
2
+ import { storage, updateSyncRulesFromYaml } from '@powersync/service-core';
3
+ import { test_utils } from '@powersync/service-core-tests';
4
+ import * as bson from 'bson';
5
+ import { describe, expect, test } from 'vitest';
6
+ import { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
7
+ import { ReplicationStreamDocumentV3 } from '../../src/storage/implementation/v3/models.js';
8
+ import { INITIALIZED_MONGO_STORAGE_FACTORY } from './util.js';
9
+
10
+ function sourceDescriptor(name: string, options: { objectId?: string } = {}): storage.SourceEntityDescriptor {
11
+ return {
12
+ connectionTag: storage.SourceTable.DEFAULT_TAG,
13
+ objectId: options.objectId ?? name,
14
+ schema: 'public',
15
+ name,
16
+ replicaIdColumns: [
17
+ {
18
+ name: 'id',
19
+ type: 'VARCHAR',
20
+ typeId: 25
21
+ }
22
+ ]
23
+ };
24
+ }
25
+
26
+ function objectIdGenerator(id: string) {
27
+ let used = false;
28
+ return () => {
29
+ if (used) {
30
+ throw new Error(`Can only generate a single id using ${id}`);
31
+ }
32
+ used = true;
33
+ return new bson.ObjectId(id);
34
+ };
35
+ }
36
+
37
+ async function collectionExists(db: VersionedPowerSyncMongoV3, name: string) {
38
+ return await db.db.listCollections({ name }, { nameOnly: true }).hasNext();
39
+ }
40
+
41
+ async function cleanupStoppedSyncConfigs(bucketStorage: MongoSyncBucketStorageV3) {
42
+ return bucketStorage.cleanupStoppedSyncConfigs({
43
+ defaultSchema: 'public',
44
+ sourceConnectionTag: storage.SourceTable.DEFAULT_TAG
45
+ });
46
+ }
47
+
48
+ describe('cleanupStoppedSyncConfigs - mongodb', () => {
49
+ test('cleans up unused stopped sync config storage', async () => {
50
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
51
+
52
+ const first = await factory.updateSyncRules(
53
+ updateSyncRulesFromYaml(
54
+ `
55
+ config:
56
+ edition: 3
57
+
58
+ streams:
59
+ by_project:
60
+ query: |
61
+ SELECT id, project FROM "Scene"
62
+ WHERE project IN (
63
+ SELECT project FROM "ProjectInvitation"
64
+ WHERE project = subscription.parameter('project')
65
+ )
66
+ `,
67
+ { storageVersion: 3 }
68
+ )
69
+ );
70
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
71
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
72
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
73
+
74
+ const sceneTable = (
75
+ await firstWriter.resolveTables({
76
+ connection_id: 1,
77
+ source: sourceDescriptor('Scene', { objectId: 'scene-relation' }),
78
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38350')
79
+ })
80
+ ).tables[0];
81
+ const invitationTable = (
82
+ await firstWriter.resolveTables({
83
+ connection_id: 1,
84
+ source: sourceDescriptor('ProjectInvitation', { objectId: 'project-invitation-relation' }),
85
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38351')
86
+ })
87
+ ).tables[0];
88
+ await firstWriter.save({
89
+ sourceTable: sceneTable,
90
+ tag: storage.SaveOperationTag.INSERT,
91
+ after: {
92
+ id: 'scene-1',
93
+ project: 'project-1'
94
+ },
95
+ afterReplicaId: test_utils.rid('scene-1')
96
+ });
97
+ await firstWriter.save({
98
+ sourceTable: invitationTable,
99
+ tag: storage.SaveOperationTag.INSERT,
100
+ after: {
101
+ id: 'invitation-1',
102
+ project: 'project-1'
103
+ },
104
+ afterReplicaId: test_utils.rid('invitation-1')
105
+ });
106
+ await firstWriter.markAllSnapshotDone('1/1');
107
+ await firstWriter.commit('1/1');
108
+
109
+ const stoppedMapping = first.syncConfigContent[0].mapping;
110
+ const stoppedBucketDefinitionId = stoppedMapping.allBucketDefinitionIds()[0];
111
+ const stoppedParameterIndexId = stoppedMapping.allParameterIndexIds()[0];
112
+ expect(stoppedBucketDefinitionId).toBeDefined();
113
+ expect(stoppedParameterIndexId).toBeDefined();
114
+ const bucketDataCollection = db.bucketData(first.replicationStreamId, stoppedBucketDefinitionId!).collectionName;
115
+ const parameterIndexCollection = db.parameterIndex(
116
+ first.replicationStreamId,
117
+ stoppedParameterIndexId!
118
+ ).collectionName;
119
+ const sceneRecordsCollection = db.sourceRecords(
120
+ first.replicationStreamId,
121
+ sceneTable.id as bson.ObjectId
122
+ ).collectionName;
123
+ const invitationRecordsCollection = db.sourceRecords(
124
+ first.replicationStreamId,
125
+ invitationTable.id as bson.ObjectId
126
+ ).collectionName;
127
+ expect(await collectionExists(db, bucketDataCollection)).toBe(true);
128
+ expect(await collectionExists(db, parameterIndexCollection)).toBe(true);
129
+ expect(await collectionExists(db, sceneRecordsCollection)).toBe(true);
130
+ expect(await collectionExists(db, invitationRecordsCollection)).toBe(true);
131
+
132
+ const second = await factory.updateSyncRules(
133
+ updateSyncRulesFromYaml(
134
+ `
135
+ config:
136
+ edition: 3
137
+
138
+ streams:
139
+ by_owner:
140
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
141
+ `,
142
+ { storageVersion: 3 }
143
+ )
144
+ );
145
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
146
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
147
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
148
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
149
+ await secondWriter.markAllSnapshotDone('2/1');
150
+ await secondWriter.commit('2/1');
151
+
152
+ const result = await cleanupStoppedSyncConfigs(
153
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
154
+ );
155
+
156
+ expect(result).toEqual({
157
+ stoppedSyncConfigsRemoved: 1,
158
+ bucketDataCollectionsDropped: 1,
159
+ parameterIndexCollectionsDropped: 1,
160
+ bucketStateDocumentsDeleted: 1,
161
+ sourceRecordCollectionsDropped: 2,
162
+ sourceTablesUpdated: 0,
163
+ sourceTablesDeleted: 2
164
+ });
165
+ expect(await collectionExists(db, bucketDataCollection)).toBe(false);
166
+ expect(await collectionExists(db, parameterIndexCollection)).toBe(false);
167
+ expect(await collectionExists(db, sceneRecordsCollection)).toBe(false);
168
+ expect(await collectionExists(db, invitationRecordsCollection)).toBe(false);
169
+ expect(
170
+ await db.sourceTables(first.replicationStreamId).countDocuments({ _id: sceneTable.id as bson.ObjectId })
171
+ ).toBe(0);
172
+ expect(
173
+ await db.sourceTables(first.replicationStreamId).countDocuments({ _id: invitationTable.id as bson.ObjectId })
174
+ ).toBe(0);
175
+
176
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
177
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
178
+ });
179
+
180
+ test('keeps source table membership still used by a live sync config', async () => {
181
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
182
+
183
+ const first = await factory.updateSyncRules(
184
+ updateSyncRulesFromYaml(
185
+ `
186
+ config:
187
+ edition: 3
188
+
189
+ streams:
190
+ by_owner:
191
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
192
+ by_project:
193
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
194
+ `,
195
+ { storageVersion: 3 }
196
+ )
197
+ );
198
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
199
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
200
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
201
+ const resolved = await firstWriter.resolveTables({
202
+ connection_id: 1,
203
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
204
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38352')
205
+ });
206
+ await firstWriter.save({
207
+ sourceTable: resolved.tables[0],
208
+ tag: storage.SaveOperationTag.INSERT,
209
+ after: {
210
+ id: 'todo-1',
211
+ owner_id: 'user-1',
212
+ project_id: 'project-1'
213
+ },
214
+ afterReplicaId: test_utils.rid('todo-1')
215
+ });
216
+ await firstWriter.markAllSnapshotDone('1/1');
217
+ await firstWriter.commit('1/1');
218
+
219
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
220
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
221
+ const [ownerDefinitionId, projectDefinitionId] = first.syncConfigContent[0].mapping.allBucketDefinitionIds();
222
+ const ownerBucketDataCollection = db.bucketData(first.replicationStreamId, ownerDefinitionId).collectionName;
223
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(true);
224
+ expect(
225
+ (await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId }))?.bucket_data_source_ids
226
+ ).toEqual([ownerDefinitionId, projectDefinitionId]);
227
+
228
+ const second = await factory.updateSyncRules(
229
+ updateSyncRulesFromYaml(
230
+ `
231
+ config:
232
+ edition: 3
233
+
234
+ streams:
235
+ by_project:
236
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
237
+ `,
238
+ { storageVersion: 3 }
239
+ )
240
+ );
241
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
242
+ const secondDefinitionId = second.syncConfigContent[1].mapping.allBucketDefinitionIds()[0];
243
+ expect(secondDefinitionId).toBe(projectDefinitionId);
244
+
245
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
246
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
247
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
248
+ await secondWriter.markAllSnapshotDone('2/1');
249
+ await secondWriter.commit('2/1');
250
+
251
+ const result = await cleanupStoppedSyncConfigs(
252
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
253
+ );
254
+
255
+ expect(result).toEqual({
256
+ stoppedSyncConfigsRemoved: 1,
257
+ bucketDataCollectionsDropped: 1,
258
+ parameterIndexCollectionsDropped: 0,
259
+ bucketStateDocumentsDeleted: 1,
260
+ sourceRecordCollectionsDropped: 0,
261
+ sourceTablesUpdated: 1,
262
+ sourceTablesDeleted: 0
263
+ });
264
+ const sourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
265
+ expect(sourceTable?.bucket_data_source_ids).toEqual([secondDefinitionId]);
266
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(false);
267
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
268
+
269
+ const activeStorage = (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3;
270
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
271
+ const activeTable = (
272
+ await activeWriter.resolveTables({
273
+ connection_id: 1,
274
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
275
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38354')
276
+ })
277
+ ).tables[0];
278
+ await activeWriter.save({
279
+ sourceTable: activeTable,
280
+ tag: storage.SaveOperationTag.UPDATE,
281
+ before: {
282
+ id: 'todo-1'
283
+ },
284
+ after: {
285
+ id: 'todo-1',
286
+ owner_id: 'user-1',
287
+ project_id: 'project-2'
288
+ },
289
+ beforeReplicaId: test_utils.rid('todo-1'),
290
+ afterReplicaId: test_utils.rid('todo-1')
291
+ });
292
+ await activeWriter.commit('3/1');
293
+
294
+ expect(await collectionExists(db, ownerBucketDataCollection)).toBe(false);
295
+ const sourceRecord = await db.sourceRecords(first.replicationStreamId, sourceTableId).findOne({
296
+ _id: test_utils.rid('todo-1') as any
297
+ });
298
+ expect(sourceRecord?.buckets.map((bucket) => bucket.def)).toEqual([secondDefinitionId]);
299
+ });
300
+
301
+ test('drops current_data when a table becomes event-only but is kept by a live event', async () => {
302
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
303
+
304
+ // The first config syncs `todos` data and also fires events for it.
305
+ const first = await factory.updateSyncRules(
306
+ updateSyncRulesFromYaml(
307
+ `
308
+ config:
309
+ edition: 3
310
+
311
+ streams:
312
+ by_owner:
313
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
314
+
315
+ event_definitions:
316
+ audit:
317
+ payloads:
318
+ - SELECT id, owner_id FROM todos
319
+ `,
320
+ { storageVersion: 3 }
321
+ )
322
+ );
323
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
324
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
325
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
326
+ const resolved = await firstWriter.resolveTables({
327
+ connection_id: 1,
328
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
329
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38370')
330
+ });
331
+ await firstWriter.save({
332
+ sourceTable: resolved.tables[0],
333
+ tag: storage.SaveOperationTag.INSERT,
334
+ after: {
335
+ id: 'todo-1',
336
+ owner_id: 'user-1'
337
+ },
338
+ afterReplicaId: test_utils.rid('todo-1')
339
+ });
340
+ await firstWriter.markAllSnapshotDone('1/1');
341
+ await firstWriter.commit('1/1');
342
+
343
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
344
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
345
+ const ownerDefinitionId = first.syncConfigContent[0].mapping.allBucketDefinitionIds()[0];
346
+ expect(
347
+ (await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId }))?.bucket_data_source_ids
348
+ ).toEqual([ownerDefinitionId]);
349
+ // The prior snapshot is persisted in current_data.
350
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
351
+
352
+ // The second (active) config drops the `todos` data stream but keeps the audit event, so the
353
+ // `todos` source table becomes event-only while staying alive for the live event.
354
+ const second = await factory.updateSyncRules(
355
+ updateSyncRulesFromYaml(
356
+ `
357
+ config:
358
+ edition: 3
359
+
360
+ streams:
361
+ by_id:
362
+ query: SELECT * FROM other WHERE id = subscription.parameter('id')
363
+
364
+ event_definitions:
365
+ audit:
366
+ payloads:
367
+ - SELECT id, owner_id FROM todos
368
+ `,
369
+ { storageVersion: 3 }
370
+ )
371
+ );
372
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
373
+
374
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
375
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
376
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
377
+ await secondWriter.markAllSnapshotDone('2/1');
378
+ await secondWriter.commit('2/1');
379
+
380
+ const result = await cleanupStoppedSyncConfigs(
381
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
382
+ );
383
+
384
+ expect(result).toEqual({
385
+ stoppedSyncConfigsRemoved: 1,
386
+ bucketDataCollectionsDropped: 1,
387
+ parameterIndexCollectionsDropped: 0,
388
+ bucketStateDocumentsDeleted: 1,
389
+ sourceRecordCollectionsDropped: 1,
390
+ sourceTablesUpdated: 1,
391
+ sourceTablesDeleted: 0
392
+ });
393
+
394
+ // The source table row survives (the live event still needs it), narrowed to empty
395
+ // memberships, but its now-unused current_data collection is dropped.
396
+ const sourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId });
397
+ expect(sourceTable).not.toBeNull();
398
+ expect(sourceTable?.bucket_data_source_ids).toEqual([]);
399
+ expect(sourceTable?.parameter_lookup_source_ids).toEqual([]);
400
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(false);
401
+
402
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
403
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
404
+ });
405
+
406
+ test('cleans up event-only source tables no longer triggered by a live sync config', async () => {
407
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
408
+
409
+ // The first config syncs `todos` and additionally fires events for `audit_log`. The
410
+ // `audit_log` table is referenced only by the event trigger, so its source table carries
411
+ // empty membership arrays (an event-only table).
412
+ const first = await factory.updateSyncRules(
413
+ updateSyncRulesFromYaml(
414
+ `
415
+ config:
416
+ edition: 3
417
+
418
+ streams:
419
+ by_owner:
420
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
421
+
422
+ event_definitions:
423
+ audit:
424
+ payloads:
425
+ - SELECT id FROM audit_log
426
+ `,
427
+ { storageVersion: 3 }
428
+ )
429
+ );
430
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
431
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
432
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
433
+
434
+ const todosTable = (
435
+ await firstWriter.resolveTables({
436
+ connection_id: 1,
437
+ source: sourceDescriptor('todos', { objectId: 'todos-relation' }),
438
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38360')
439
+ })
440
+ ).tables[0];
441
+ const auditTable = (
442
+ await firstWriter.resolveTables({
443
+ connection_id: 1,
444
+ source: sourceDescriptor('audit_log', { objectId: 'audit-log-relation' }),
445
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38361')
446
+ })
447
+ ).tables[0];
448
+ await firstWriter.save({
449
+ sourceTable: todosTable,
450
+ tag: storage.SaveOperationTag.INSERT,
451
+ after: {
452
+ id: 'todo-1',
453
+ owner_id: 'user-1'
454
+ },
455
+ afterReplicaId: test_utils.rid('todo-1')
456
+ });
457
+ await firstWriter.save({
458
+ sourceTable: auditTable,
459
+ tag: storage.SaveOperationTag.INSERT,
460
+ after: {
461
+ id: 'audit-1'
462
+ },
463
+ afterReplicaId: test_utils.rid('audit-1')
464
+ });
465
+ await firstWriter.markAllSnapshotDone('1/1');
466
+ await firstWriter.commit('1/1');
467
+
468
+ const todosTableId = todosTable.id as bson.ObjectId;
469
+ const auditTableId = auditTable.id as bson.ObjectId;
470
+ // The event-only table is persisted with empty membership arrays, so the membership filter
471
+ // never selects it.
472
+ const auditSourceTable = await db.sourceTables(first.replicationStreamId).findOne({ _id: auditTableId });
473
+ expect(auditSourceTable?.bucket_data_source_ids).toEqual([]);
474
+ expect(auditSourceTable?.parameter_lookup_source_ids).toEqual([]);
475
+ const auditRecordsCollection = db.sourceRecords(first.replicationStreamId, auditTableId).collectionName;
476
+
477
+ // The second (active) config keeps the same `by_owner` stream but drops the audit event.
478
+ // The shared bucket definition stays in use, so no bucket/parameter ids become unused and
479
+ // the membership-cleanup block is skipped entirely.
480
+ const second = await factory.updateSyncRules(
481
+ updateSyncRulesFromYaml(
482
+ `
483
+ config:
484
+ edition: 3
485
+
486
+ streams:
487
+ by_owner:
488
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
489
+ `,
490
+ { storageVersion: 3 }
491
+ )
492
+ );
493
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
494
+
495
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
496
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
497
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
498
+ await secondWriter.markAllSnapshotDone('2/1');
499
+ await secondWriter.commit('2/1');
500
+
501
+ const result = await cleanupStoppedSyncConfigs(
502
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
503
+ );
504
+
505
+ expect(result).toEqual({
506
+ stoppedSyncConfigsRemoved: 1,
507
+ bucketDataCollectionsDropped: 0,
508
+ parameterIndexCollectionsDropped: 0,
509
+ bucketStateDocumentsDeleted: 0,
510
+ sourceRecordCollectionsDropped: 1,
511
+ sourceTablesUpdated: 0,
512
+ sourceTablesDeleted: 1
513
+ });
514
+ // The orphaned event-only table and its source records are removed...
515
+ expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: auditTableId })).toBe(0);
516
+ expect(await collectionExists(db, auditRecordsCollection)).toBe(false);
517
+ // ...while the data table still used by the live config is retained.
518
+ expect(await db.sourceTables(first.replicationStreamId).countDocuments({ _id: todosTableId })).toBe(1);
519
+
520
+ const streamDoc = (await db.sync_rules.findOne({ _id: first.replicationStreamId })) as ReplicationStreamDocumentV3;
521
+ expect(streamDoc.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ACTIVE]);
522
+ });
523
+
524
+ test('does not recreate dropped parameter indexes from stale source record lookups', async () => {
525
+ await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
526
+
527
+ const first = await factory.updateSyncRules(
528
+ updateSyncRulesFromYaml(
529
+ `
530
+ config:
531
+ edition: 3
532
+
533
+ streams:
534
+ by_role:
535
+ query: |
536
+ SELECT * FROM todos
537
+ WHERE role_id IN (
538
+ SELECT role_id FROM memberships
539
+ WHERE role_id = subscription.parameter('role_id')
540
+ )
541
+ by_org:
542
+ query: |
543
+ SELECT * FROM todos
544
+ WHERE org_id IN (
545
+ SELECT org_id FROM memberships
546
+ WHERE org_id = subscription.parameter('org_id')
547
+ )
548
+ `,
549
+ { storageVersion: 3 }
550
+ )
551
+ );
552
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorageV3;
553
+ const db = firstStorage.db as VersionedPowerSyncMongoV3;
554
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
555
+ const resolved = await firstWriter.resolveTables({
556
+ connection_id: 1,
557
+ source: sourceDescriptor('memberships', { objectId: 'memberships-relation' }),
558
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38355')
559
+ });
560
+ await firstWriter.save({
561
+ sourceTable: resolved.tables[0],
562
+ tag: storage.SaveOperationTag.INSERT,
563
+ after: {
564
+ id: 'membership-1',
565
+ org_id: 'org-1',
566
+ role_id: 'role-1'
567
+ },
568
+ afterReplicaId: test_utils.rid('membership-1')
569
+ });
570
+ await firstWriter.markAllSnapshotDone('1/1');
571
+ await firstWriter.commit('1/1');
572
+
573
+ const sourceTableId = resolved.tables[0].id as bson.ObjectId;
574
+ const sourceRecordsCollection = db.sourceRecords(first.replicationStreamId, sourceTableId).collectionName;
575
+ const [roleIndexId, orgIndexId] = first.syncConfigContent[0].mapping.allParameterIndexIds();
576
+ const orgParameterIndexCollection = db.parameterIndex(first.replicationStreamId, orgIndexId).collectionName;
577
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(true);
578
+ expect(
579
+ (await db.sourceTables(first.replicationStreamId).findOne({ _id: sourceTableId }))?.parameter_lookup_source_ids
580
+ ).toEqual([roleIndexId, orgIndexId]);
581
+
582
+ const second = await factory.updateSyncRules(
583
+ updateSyncRulesFromYaml(
584
+ `
585
+ config:
586
+ edition: 3
587
+
588
+ streams:
589
+ by_role:
590
+ query: |
591
+ SELECT * FROM todos
592
+ WHERE role_id IN (
593
+ SELECT role_id FROM memberships
594
+ WHERE role_id = subscription.parameter('role_id')
595
+ )
596
+ `,
597
+ { storageVersion: 3 }
598
+ )
599
+ );
600
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
601
+ const secondIndexId = second.syncConfigContent[1].mapping.allParameterIndexIds()[0];
602
+ expect(secondIndexId).toBe(roleIndexId);
603
+
604
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
605
+ const secondStorage = factory.getInstance(replicatingStreams[0]) as MongoSyncBucketStorageV3;
606
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
607
+ await secondWriter.markAllSnapshotDone('2/1');
608
+ await secondWriter.commit('2/1');
609
+
610
+ const result = await cleanupStoppedSyncConfigs(
611
+ (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3
612
+ );
613
+ expect(result.parameterIndexCollectionsDropped).toBe(1);
614
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(false);
615
+ expect(await collectionExists(db, sourceRecordsCollection)).toBe(true);
616
+
617
+ const activeStorage = (await factory.getActiveSyncConfig())!.storage as MongoSyncBucketStorageV3;
618
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
619
+ const activeTable = (
620
+ await activeWriter.resolveTables({
621
+ connection_id: 1,
622
+ source: sourceDescriptor('memberships', { objectId: 'memberships-relation' }),
623
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38357')
624
+ })
625
+ ).tables[0];
626
+ await activeWriter.save({
627
+ sourceTable: activeTable,
628
+ tag: storage.SaveOperationTag.UPDATE,
629
+ before: {
630
+ id: 'membership-1'
631
+ },
632
+ after: {
633
+ id: 'membership-1',
634
+ org_id: 'org-1',
635
+ role_id: 'role-2'
636
+ },
637
+ beforeReplicaId: test_utils.rid('membership-1'),
638
+ afterReplicaId: test_utils.rid('membership-1')
639
+ });
640
+ await activeWriter.commit('3/1');
641
+
642
+ expect(await collectionExists(db, orgParameterIndexCollection)).toBe(false);
643
+ const sourceRecord = await db.sourceRecords(first.replicationStreamId, sourceTableId).findOne({
644
+ _id: test_utils.rid('membership-1') as any
645
+ });
646
+ expect(sourceRecord?.lookups.map((lookup) => lookup.i)).toEqual([secondIndexId]);
647
+ });
648
+ });
@@ -9,7 +9,11 @@ for (let storageVersion of TEST_STORAGE_VERSIONS) {
9
9
  register.registerDataStorageParameterTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion }));
10
10
 
11
11
  describe(`Mongo Sync Bucket Storage - Data - v${storageVersion}`, () =>
12
- register.registerDataStorageDataTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion }));
12
+ register.registerDataStorageDataTests({
13
+ ...INITIALIZED_MONGO_STORAGE_FACTORY,
14
+ storageVersion,
15
+ compressedBucketStorage: storageVersion >= 3
16
+ }));
13
17
 
14
18
  describe(`Mongo Sync Bucket Storage - Checkpoints - v${storageVersion}`, () =>
15
19
  register.registerDataStorageCheckpointTests({ ...INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion }));
@@ -22,11 +26,9 @@ describe('Mongo Sync Bucket Storage - split operations', () =>
22
26
  mongoTestStorageFactoryGenerator({
23
27
  url: env.MONGO_TEST_URL,
24
28
  isCI: env.CI,
25
- internalOptions: {
26
- checksumOptions: {
27
- bucketBatchLimit: 100,
28
- operationBatchLimit: 1
29
- }
29
+ checksumOptions: {
30
+ bucketBatchLimit: 100,
31
+ operationBatchLimit: 1
30
32
  }
31
33
  })
32
34
  ));
@@ -36,11 +38,9 @@ describe('Mongo Sync Bucket Storage - split buckets', () =>
36
38
  mongoTestStorageFactoryGenerator({
37
39
  url: env.MONGO_TEST_URL,
38
40
  isCI: env.CI,
39
- internalOptions: {
40
- checksumOptions: {
41
- bucketBatchLimit: 1,
42
- operationBatchLimit: 100
43
- }
41
+ checksumOptions: {
42
+ bucketBatchLimit: 1,
43
+ operationBatchLimit: 100
44
44
  }
45
45
  })
46
46
  ));