@powersync/service-module-mongodb-storage 0.20.0 → 0.22.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 (192) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/storage/MongoBucketStorage.d.ts +36 -3
  3. package/dist/storage/MongoBucketStorage.js +372 -207
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +17 -5
  6. package/dist/storage/implementation/MongoBucketBatch.js +44 -17
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  12. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  13. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  14. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  15. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
  16. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  17. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
  18. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  19. package/dist/storage/implementation/MongoStorageProvider.js +7 -3
  20. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  21. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
  22. package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
  23. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  24. package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
  25. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  26. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  27. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  28. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  29. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  30. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  31. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  32. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
  33. package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
  34. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
  35. package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
  36. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  37. package/dist/storage/implementation/common/PersistedBatch.js +49 -6
  38. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  39. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  40. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  41. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  42. package/dist/storage/implementation/db.d.ts +9 -1
  43. package/dist/storage/implementation/db.js +27 -1
  44. package/dist/storage/implementation/db.js.map +1 -1
  45. package/dist/storage/implementation/models.d.ts +31 -0
  46. package/dist/storage/implementation/models.js.map +1 -1
  47. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
  48. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +33 -4
  49. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  50. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  51. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  52. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  53. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  54. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  55. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  56. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  57. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
  58. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  59. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  60. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  61. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  62. package/dist/storage/implementation/v1/models.js +1 -0
  63. package/dist/storage/implementation/v1/models.js.map +1 -1
  64. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  65. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  66. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  67. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +10 -2
  68. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +100 -26
  69. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  70. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +82 -25
  71. package/dist/storage/implementation/v3/MongoCompactorV3.js +698 -159
  72. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  73. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  74. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  75. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  76. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  77. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  78. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  79. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  80. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
  81. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  82. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  83. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  84. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  85. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  86. package/dist/storage/implementation/v3/PersistedBatchV3.js +76 -42
  87. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  88. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  89. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  90. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  91. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  92. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  93. package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
  94. package/dist/storage/implementation/v3/compact-utils.js +216 -0
  95. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  96. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  97. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  98. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  99. package/dist/storage/implementation/v3/models.d.ts +84 -4
  100. package/dist/storage/implementation/v3/models.js.map +1 -1
  101. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  102. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  103. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  104. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  105. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  106. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  107. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  108. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  109. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  110. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  111. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  112. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  113. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  114. package/dist/storage/implementation/v3/source-table-utils.d.ts +6 -6
  115. package/dist/storage/implementation/v3/source-table-utils.js +30 -37
  116. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  117. package/dist/storage/storage-index.d.ts +2 -1
  118. package/dist/storage/storage-index.js +1 -0
  119. package/dist/storage/storage-index.js.map +1 -1
  120. package/dist/types/types.d.ts +28 -1
  121. package/dist/types/types.js +28 -7
  122. package/dist/types/types.js.map +1 -1
  123. package/dist/utils/test-utils.js +1 -0
  124. package/dist/utils/test-utils.js.map +1 -1
  125. package/dist/utils/util.js +3 -1
  126. package/dist/utils/util.js.map +1 -1
  127. package/package.json +10 -9
  128. package/src/storage/MongoBucketStorage.ts +507 -269
  129. package/src/storage/implementation/MongoBucketBatch.ts +67 -24
  130. package/src/storage/implementation/MongoCompactor.ts +35 -346
  131. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  132. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
  133. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
  134. package/src/storage/implementation/MongoStorageProvider.ts +15 -3
  135. package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
  136. package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
  137. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  138. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  139. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  140. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  141. package/src/storage/implementation/common/PersistedBatch.ts +66 -10
  142. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  143. package/src/storage/implementation/db.ts +35 -2
  144. package/src/storage/implementation/models.ts +34 -0
  145. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +46 -6
  146. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  147. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  148. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
  149. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  150. package/src/storage/implementation/v1/models.ts +1 -0
  151. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  152. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +138 -34
  153. package/src/storage/implementation/v3/MongoCompactorV3.ts +958 -225
  154. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  155. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  156. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
  157. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  158. package/src/storage/implementation/v3/PersistedBatchV3.ts +93 -48
  159. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  160. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  161. package/src/storage/implementation/v3/compact-utils.ts +320 -0
  162. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  163. package/src/storage/implementation/v3/models.ts +91 -3
  164. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  165. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  166. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  167. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  168. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  169. package/src/storage/implementation/v3/source-table-utils.ts +39 -41
  170. package/src/storage/storage-index.ts +2 -0
  171. package/src/types/types.ts +42 -7
  172. package/src/utils/test-utils.ts +1 -0
  173. package/src/utils/util.ts +3 -1
  174. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  175. package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
  176. package/test/src/chunk_compaction_config.test.ts +38 -0
  177. package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
  178. package/test/src/compact-utils.test.ts +226 -0
  179. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  180. package/test/src/object_storage_usage.test.ts +267 -0
  181. package/test/src/parameter_compacting_v1.test.ts +120 -0
  182. package/test/src/parameter_compacting_v3.test.ts +192 -0
  183. package/test/src/parameter_compaction_fence.test.ts +276 -0
  184. package/test/src/storage.test.ts +273 -22
  185. package/test/src/storage_compacting.test.ts +968 -246
  186. package/test/src/storage_s3_checksums.test.ts +3 -4
  187. package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
  188. package/test/src/storage_s3_reading.test.ts +290 -2
  189. package/test/src/storage_s3_writing.test.ts +1 -0
  190. package/test/src/storage_sync.test.ts +336 -41
  191. package/test/src/util.ts +1 -1
  192. package/tsconfig.tsbuildinfo +1 -1
@@ -16,6 +16,7 @@ import { MongoPersistedSyncConfigContentV3 } from '../../src/storage/implementat
16
16
  import { BucketDataDoc, BucketKey } from '../../src/storage/implementation/common/BucketDataDoc.js';
17
17
  import { MongoSyncBucketStorage } from '../../src/storage/implementation/createMongoSyncBucketStorage.js';
18
18
  import { getMongoStorageConfig } from '../../src/storage/implementation/models.js';
19
+ import { loadBucketDataDocumentV1, serializeBucketDataV1 } from '../../src/storage/implementation/v1/models.js';
19
20
  import { SourceRecordStoreV3 } from '../../src/storage/implementation/v3/SourceRecordStoreV3.js';
20
21
  import type { VersionedPowerSyncMongoV3 } from '../../src/storage/implementation/v3/VersionedPowerSyncMongoV3.js';
21
22
  import { serializeBucketData } from '../../src/storage/implementation/v3/bucket-format.js';
@@ -114,6 +115,31 @@ function bucketDefinitionId(bucket: string) {
114
115
  return parseInt(match[1], 16);
115
116
  }
116
117
 
118
+ test('V1 bucket data retains an optional persisted subkey', () => {
119
+ const sourceTable = new bson.ObjectId();
120
+ const document = serializeBucketDataV1({
121
+ bucketKey: {
122
+ replicationStreamId: 1,
123
+ definitionId: '0',
124
+ bucket: 'global[]'
125
+ },
126
+ o: 1n,
127
+ op: 'PUT',
128
+ source_table: sourceTable,
129
+ source_key: 'source-key',
130
+ subkey: 'persisted-subkey',
131
+ table: 'items',
132
+ row_id: 'item-1',
133
+ checksum: 1n,
134
+ data: '{"id":"item-1"}'
135
+ });
136
+
137
+ expect(loadBucketDataDocumentV1(document).subkey).toBe('persisted-subkey');
138
+
139
+ const { subkey: _subkey, ...legacyDocument } = document;
140
+ expect(loadBucketDataDocumentV1(legacyDocument).subkey).toBeUndefined();
141
+ });
142
+
117
143
  function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, storageVersion: number) {
118
144
  register.registerSyncTests(storageConfig.factory, {
119
145
  storageVersion,
@@ -533,6 +559,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
533
559
  const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
534
560
  const dataOnlyTableId = new bson.ObjectId('6544e3899293153fa7b38348');
535
561
  const addedParameterTableId = new bson.ObjectId('6544e3899293153fa7b38349');
562
+ const addedEventTableId = new bson.ObjectId('6544e3899293153fa7b3834a');
536
563
 
537
564
  const dataOnly = await writer.resolveTables({
538
565
  connection_id: 1,
@@ -590,15 +617,13 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
590
617
  const eventOnly = await writer.resolveTables({
591
618
  connection_id: 1,
592
619
  source,
593
- idGenerator: () => {
594
- throw new Error('resolve should reuse existing v3 source table');
595
- },
620
+ idGenerator: () => addedEventTableId,
596
621
  parsedSyncConfig: eventOnlyRules
597
622
  });
598
623
 
599
- // Event-only table can re-use any existing table.
624
+ // A newly-added event gets its own source table so that its existing rows are snapshotted.
600
625
  expect(eventOnly.tables).toHaveLength(1);
601
- expect([dataOnlyTableId.toString(), addedParameterTableId.toString()]).toContain(eventOnly.tables[0].id.toString());
626
+ expect(eventOnly.tables[0].id).toEqual(addedEventTableId);
602
627
  expect(eventOnly.dropTables.map((table) => table.id)).toEqual([]);
603
628
  expect(eventOnly.tables[0].bucketDataSources).toHaveLength(0);
604
629
  expect(eventOnly.tables[0].parameterLookupSources).toHaveLength(0);
@@ -607,13 +632,12 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
607
632
  expect(eventOnly.tables[0].syncEvent).toBe(true);
608
633
  });
609
634
 
610
- test.runIf(storageVersion >= 3)(
611
- 'resolveTables designates a single event carrier across split source tables',
612
- async () => {
613
- // When memberships are split over multiple SourceTables for the same ref, a row change
614
- // is saved once per table. Only one table may fire events, otherwise the same event
615
- // would fire once per table for every row change.
616
- const dataOnlyEventYaml = `
635
+ test.runIf(storageVersion >= 3)('resolveTables assigns an event definition to one split source table', async () => {
636
+ // When memberships are split over multiple SourceTables for the same ref, a row change
637
+ // is saved once per table. Each event-definition id must belong to only one of those tables,
638
+ // otherwise that definition would be evaluated more than once for the same row change.
639
+ // Other tables may independently own and evaluate different event definitions.
640
+ const dataOnlyEventYaml = `
617
641
  bucket_definitions:
618
642
  by_owner:
619
643
  parameters:
@@ -626,7 +650,7 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
626
650
  payloads:
627
651
  - SELECT id, owner_id FROM memberships
628
652
  `;
629
- const fullEventYaml = `
653
+ const fullEventYaml = `
630
654
  bucket_definitions:
631
655
  by_owner:
632
656
  parameters:
@@ -640,42 +664,201 @@ function registerSyncStorageTests(storageConfig: storage.TestStorageConfig, stor
640
664
  - SELECT id, owner_id FROM memberships
641
665
  `;
642
666
 
667
+ await using factory = await storageConfig.factory();
668
+ const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(dataOnlyEventYaml, { storageVersion }));
669
+ const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
670
+ await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
671
+ const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyEventYaml, storageVersion);
672
+ const fullRules = parsedSyncConfigSetFor(fullEventYaml, storageVersion);
673
+ const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
674
+
675
+ const initial = await writer.resolveTables({
676
+ connection_id: 1,
677
+ source,
678
+ idGenerator: objectIdGenerator('6544e3899293153fa7b3834b'),
679
+ parsedSyncConfig: dataOnlyRules
680
+ });
681
+ expect(initial.tables).toHaveLength(1);
682
+ expect(initial.tables[0].syncEvent).toBe(true);
683
+
684
+ // Adding the table-based parameter lookup creates a second SourceTable for the same ref.
685
+ const split = await writer.resolveTables({
686
+ connection_id: 1,
687
+ source,
688
+ idGenerator: objectIdGenerator('6544e3899293153fa7b3834c'),
689
+ parsedSyncConfig: fullRules
690
+ });
691
+ expect(split.tables).toHaveLength(2);
692
+ // Both tables match the event by ref, but this event id belongs to only one of them.
693
+ const eventOwners = split.tables.filter((table) => table.syncEvent);
694
+ expect(eventOwners).toHaveLength(1);
695
+
696
+ // getSourceTableStatus rehydrates the persisted event memberships rather than recomputing
697
+ // them from the ref, so refreshing the other table does not assign this event to it.
698
+ const nonOwner = split.tables.find((table) => !table.syncEvent)!;
699
+ const refreshed = await writer.getSourceTableStatus(nonOwner);
700
+ expect(refreshed!.syncEvent).toBe(false);
701
+ });
702
+
703
+ test.runIf(storageVersion >= 3)(
704
+ 'reuses an unchanged event definition without resnapshotting or duplicate evaluation',
705
+ async () => {
706
+ const firstYaml = `
707
+ config:
708
+ edition: 3
709
+
710
+ streams:
711
+ by_owner:
712
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
713
+
714
+ event_definitions:
715
+ write_checkpoints:
716
+ payloads:
717
+ - SELECT user_id, checkpoint FROM checkpoints WHERE active = true AND checkpoint > 0
718
+ `;
719
+ const secondYaml = `
720
+ config:
721
+ edition: 3
722
+
723
+ streams:
724
+ by_owner:
725
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
726
+ by_project:
727
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
728
+
729
+ event_definitions:
730
+ write_checkpoints:
731
+ payloads:
732
+ - select "user_id", "checkpoint" from "checkpoints" where "active" = true and "checkpoint" > 0
733
+ `;
734
+
643
735
  await using factory = await storageConfig.factory();
644
- const syncRules = await factory.updateSyncRules(updateSyncRulesFromYaml(dataOnlyEventYaml, { storageVersion }));
645
- const bucketStorage = factory.getInstance(syncRules) as MongoSyncBucketStorage;
646
- await using writer = await bucketStorage.createWriter(test_utils.BATCH_OPTIONS);
647
- const dataOnlyRules = parsedSyncConfigSetFor(dataOnlyEventYaml, storageVersion);
648
- const fullRules = parsedSyncConfigSetFor(fullEventYaml, storageVersion);
649
- const source = sourceDescriptor('memberships', { objectId: 'memberships-relation' });
736
+ const emittedEvents: { name: string; eventId: string | undefined }[] = [];
737
+ const disposeListener = factory.registerListener({
738
+ replicationEvent: ({ event, event_id }) => emittedEvents.push({ name: event.name, eventId: event_id })
739
+ });
740
+ try {
741
+ const first = await factory.updateSyncRules(updateSyncRulesFromYaml(firstYaml, { storageVersion }));
742
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
743
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
744
+ const source = sourceDescriptor('checkpoints', { objectId: 'checkpoints-relation' });
745
+ const firstResolved = await firstWriter.resolveTables({
746
+ connection_id: 1,
747
+ source,
748
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38351')
749
+ });
750
+ const eventId = [...firstResolved.tables[0].eventDefinitionIds!][0];
751
+ expect(eventId).toBeDefined();
752
+ await firstWriter.markTableSnapshotDone(firstResolved.tables, '1/1');
753
+ await firstWriter.markAllSnapshotDone('1/1');
754
+ await firstWriter.commit('1/1');
755
+
756
+ const second = await factory.updateSyncRules(updateSyncRulesFromYaml(secondYaml, { storageVersion }));
757
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
758
+ const secondStorage = factory.getInstance(second) as MongoSyncBucketStorage;
759
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
760
+ const resolved = await secondWriter.resolveTables({
761
+ connection_id: 1,
762
+ source,
763
+ idGenerator: () => {
764
+ throw new Error('unchanged event definition should reuse the snapshotted source table');
765
+ }
766
+ });
767
+
768
+ expect(resolved.tables).toHaveLength(1);
769
+ expect(resolved.tables[0].id).toEqual(firstResolved.tables[0].id);
770
+ expect(resolved.tables[0].snapshotComplete).toBe(true);
771
+ expect([...resolved.tables[0].eventDefinitionIds!]).toEqual([eventId]);
772
+
773
+ emittedEvents.length = 0;
774
+ await secondWriter.save({
775
+ sourceTable: resolved.tables[0],
776
+ tag: storage.SaveOperationTag.INSERT,
777
+ after: { id: 'checkpoint-1', user_id: 'user-1', checkpoint: 1n, active: 1 },
778
+ afterReplicaId: test_utils.rid('checkpoint-1')
779
+ });
780
+ expect(emittedEvents).toEqual([{ name: 'write_checkpoints', eventId }]);
781
+ } finally {
782
+ disposeListener();
783
+ }
784
+ }
785
+ );
786
+
787
+ test.runIf(storageVersion >= 3)('creates snapshot work for a changed event definition', async () => {
788
+ const yaml = (active: boolean) => `
789
+ config:
790
+ edition: 3
650
791
 
651
- const initial = await writer.resolveTables({
792
+ streams:
793
+ by_owner:
794
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
795
+
796
+ event_definitions:
797
+ write_checkpoints:
798
+ payloads:
799
+ - SELECT user_id, checkpoint FROM checkpoints WHERE active = ${active}
800
+ `;
801
+
802
+ await using factory = await storageConfig.factory();
803
+ const emittedEvents: { name: string; eventId: string | undefined }[] = [];
804
+ const disposeListener = factory.registerListener({
805
+ replicationEvent: ({ event, event_id }) => emittedEvents.push({ name: event.name, eventId: event_id })
806
+ });
807
+ try {
808
+ const first = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml(true), { storageVersion }));
809
+ const firstStorage = factory.getInstance(first) as MongoSyncBucketStorage;
810
+ await using firstWriter = await firstStorage.createWriter(test_utils.BATCH_OPTIONS);
811
+ const source = sourceDescriptor('checkpoints', { objectId: 'checkpoints-relation' });
812
+ const firstResolved = await firstWriter.resolveTables({
652
813
  connection_id: 1,
653
814
  source,
654
- idGenerator: objectIdGenerator('6544e3899293153fa7b3834b'),
655
- parsedSyncConfig: dataOnlyRules
815
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38352')
656
816
  });
657
- expect(initial.tables).toHaveLength(1);
658
- expect(initial.tables[0].syncEvent).toBe(true);
817
+ const oldEventId = [...firstResolved.tables[0].eventDefinitionIds!][0];
818
+ await firstWriter.markTableSnapshotDone(firstResolved.tables, '1/1');
819
+ await firstWriter.markAllSnapshotDone('1/1');
820
+ await firstWriter.commit('1/1');
659
821
 
660
- // Adding the table-based parameter lookup creates a second SourceTable for the same ref.
661
- const split = await writer.resolveTables({
822
+ const second = await factory.updateSyncRules(updateSyncRulesFromYaml(yaml(false), { storageVersion }));
823
+ expect(second.replicationStreamId).toBe(first.replicationStreamId);
824
+ const secondStorage = factory.getInstance(second) as MongoSyncBucketStorage;
825
+ await using secondWriter = await secondStorage.createWriter(test_utils.BATCH_OPTIONS);
826
+ const resolved = await secondWriter.resolveTables({
662
827
  connection_id: 1,
663
828
  source,
664
- idGenerator: objectIdGenerator('6544e3899293153fa7b3834c'),
665
- parsedSyncConfig: fullRules
829
+ idGenerator: objectIdGenerator('6544e3899293153fa7b38353')
666
830
  });
667
- expect(split.tables).toHaveLength(2);
668
- // Both tables match the event by ref, but only one may carry it.
669
- const carriers = split.tables.filter((table) => table.syncEvent);
670
- expect(carriers).toHaveLength(1);
671
-
672
- // getSourceTableStatus preserves the carrier designation rather than recomputing it
673
- // from the ref, so refreshing a non-carrier table does not make it fire events.
674
- const nonCarrier = split.tables.find((table) => !table.syncEvent)!;
675
- const refreshed = await writer.getSourceTableStatus(nonCarrier);
676
- expect(refreshed!.syncEvent).toBe(false);
831
+
832
+ expect(resolved.tables).toHaveLength(2);
833
+ const oldTable = resolved.tables.find((table) => table.eventDefinitionIds?.has(oldEventId))!;
834
+ const newTable = resolved.tables.find((table) => !table.eventDefinitionIds?.has(oldEventId))!;
835
+ const newEventId = [...newTable.eventDefinitionIds!][0];
836
+ expect(newEventId).toBeDefined();
837
+ expect(newEventId).not.toBe(oldEventId);
838
+ expect(oldTable.snapshotComplete).toBe(true);
839
+ expect(newTable.snapshotComplete).toBe(false);
840
+ expect(newTable.bucketDataSources).toEqual([]);
841
+ expect(newTable.parameterLookupSources).toEqual([]);
842
+
843
+ await expect(secondWriter.markSnapshotDone('2/1')).rejects.toThrow(
844
+ 'Cannot mark snapshot done while source tables still require snapshotting'
845
+ );
846
+
847
+ emittedEvents.length = 0;
848
+ await secondWriter.save({
849
+ sourceTable: newTable,
850
+ tag: storage.SaveOperationTag.INSERT,
851
+ after: { id: 'checkpoint-2', user_id: 'user-2', checkpoint: 2n, active: 0 },
852
+ afterReplicaId: test_utils.rid('checkpoint-2')
853
+ });
854
+ expect(emittedEvents).toEqual([{ name: 'write_checkpoints', eventId: newEventId }]);
855
+
856
+ await secondWriter.markTableSnapshotDone([newTable], '2/1');
857
+ await secondWriter.markSnapshotDone('2/1');
858
+ } finally {
859
+ disposeListener();
677
860
  }
678
- );
861
+ });
679
862
 
680
863
  test.runIf(storageVersion >= 3)('uses v3 mongodb model shapes', async () => {
681
864
  await using factory = await storageConfig.factory();
@@ -1159,6 +1342,100 @@ streams:
1159
1342
  }
1160
1343
  );
1161
1344
 
1345
+ test.runIf(storageVersion >= 3)(
1346
+ 'restart creates a replacement stream instead of appending to the active stream',
1347
+ async () => {
1348
+ await using factory = await storageConfig.factory();
1349
+
1350
+ const active = await factory.updateSyncRules(
1351
+ updateSyncRulesFromYaml(
1352
+ `
1353
+ config:
1354
+ edition: 3
1355
+
1356
+ streams:
1357
+ by_owner:
1358
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
1359
+ `,
1360
+ { storageVersion }
1361
+ )
1362
+ );
1363
+ const activeStorage = factory.getInstance(active) as MongoSyncBucketStorage;
1364
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
1365
+ await activeWriter.markAllSnapshotDone('1/1');
1366
+ await activeWriter.commit('1/1');
1367
+
1368
+ await factory.restartReplication(active.replicationStreamId);
1369
+
1370
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
1371
+ expect(replicatingStreams).toHaveLength(1);
1372
+ expect(replicatingStreams[0].replicationStreamId).not.toEqual(active.replicationStreamId);
1373
+ expect(replicatingStreams[0].state).toBe(storage.SyncRuleState.PROCESSING);
1374
+
1375
+ const oldStream = (await (factory as MongoBucketStorage).db.sync_rules.findOne({
1376
+ _id: active.replicationStreamId
1377
+ })) as ReplicationStreamDocumentV3;
1378
+ expect(oldStream.state).toBe(storage.SyncRuleState.ERRORED);
1379
+ expect(oldStream.sync_configs.map((config) => config.state)).toEqual([storage.SyncRuleState.ERRORED]);
1380
+ }
1381
+ );
1382
+
1383
+ test.runIf(storageVersion >= 3)('restart errors an active stream with an embedded deploying config', async () => {
1384
+ await using factory = await storageConfig.factory();
1385
+
1386
+ const active = await factory.updateSyncRules(
1387
+ updateSyncRulesFromYaml(
1388
+ `
1389
+ config:
1390
+ edition: 3
1391
+
1392
+ streams:
1393
+ by_owner:
1394
+ query: SELECT * FROM todos WHERE owner_id = subscription.parameter('owner_id')
1395
+ `,
1396
+ { storageVersion }
1397
+ )
1398
+ );
1399
+ const activeStorage = factory.getInstance(active) as MongoSyncBucketStorage;
1400
+ await using activeWriter = await activeStorage.createWriter(test_utils.BATCH_OPTIONS);
1401
+ await activeWriter.markAllSnapshotDone('1/1');
1402
+ await activeWriter.commit('1/1');
1403
+
1404
+ const deploying = await factory.updateSyncRules(
1405
+ updateSyncRulesFromYaml(
1406
+ `
1407
+ config:
1408
+ edition: 3
1409
+
1410
+ streams:
1411
+ by_project:
1412
+ query: SELECT * FROM todos WHERE project_id = subscription.parameter('project_id')
1413
+ `,
1414
+ { storageVersion }
1415
+ )
1416
+ );
1417
+ expect(deploying.replicationStreamId).toEqual(active.replicationStreamId);
1418
+
1419
+ await factory.restartReplication(active.replicationStreamId);
1420
+
1421
+ const replicatingStreams = await factory.getReplicatingReplicationStreams();
1422
+ expect(replicatingStreams).toHaveLength(1);
1423
+ expect(replicatingStreams[0].replicationStreamId).not.toEqual(active.replicationStreamId);
1424
+
1425
+ const oldStream = (await (factory as MongoBucketStorage).db.sync_rules.findOne({
1426
+ _id: active.replicationStreamId
1427
+ })) as ReplicationStreamDocumentV3;
1428
+ expect(oldStream.state).toBe(storage.SyncRuleState.ERRORED);
1429
+ expect(oldStream.sync_configs.map((config) => config.state)).toEqual([
1430
+ storage.SyncRuleState.ERRORED,
1431
+ storage.SyncRuleState.STOP
1432
+ ]);
1433
+ const oldActive = await factory.getActiveSyncConfig();
1434
+ expect(oldActive).not.toBeNull();
1435
+ expect(oldActive!.replicationStream.syncConfigContent).toHaveLength(1);
1436
+ await expect(oldActive!.storage.getCheckpoint()).resolves.toBeDefined();
1437
+ });
1438
+
1162
1439
  test.runIf(storageVersion >= 3)(
1163
1440
  'appended compatible config adopts the stream checkpoint instead of regressing to 0',
1164
1441
  async () => {
@@ -1624,6 +1901,22 @@ streams:
1624
1901
  expect(metricsAfter.replication_size_bytes).toBeGreaterThan(metricsBefore.replication_size_bytes);
1625
1902
  });
1626
1903
 
1904
+ test.runIf(storageVersion >= 3)('storage metrics include the sync config version label', async () => {
1905
+ await using factory = await storageConfig.factory();
1906
+ const syncRules = await factory.updateSyncRules(
1907
+ updateSyncRulesFromYaml(MINIMAL_SYNC_RULES, { storageVersion, version_label: 'metrics-test' })
1908
+ );
1909
+
1910
+ const metrics = await factory.getStorageMetrics();
1911
+
1912
+ expect(metrics.sync_config_metrics).toContainEqual(
1913
+ expect.objectContaining({
1914
+ sync_config_id: syncRules.syncConfigContent[0].syncConfigId,
1915
+ version_label: 'metrics-test'
1916
+ })
1917
+ );
1918
+ });
1919
+
1627
1920
  test.runIf(storageVersion >= 3)(
1628
1921
  'loads parameter checkpoint changes across all v3 parameter index collections',
1629
1922
  async () => {
@@ -1676,7 +1969,7 @@ streams:
1676
1969
  }
1677
1970
  );
1678
1971
 
1679
- test.runIf(storageVersion == 3)('cleans pending deletes only for tracked v3 source tables', async () => {
1972
+ test.runIf(storageVersion >= 3)('cleans pending deletes only for tracked v3 source tables', async () => {
1680
1973
  await using factory = await storageConfig.factory();
1681
1974
  const syncRules = await factory.updateSyncRules(
1682
1975
  updateSyncRulesFromYaml(
@@ -1709,6 +2002,7 @@ streams:
1709
2002
  snapshot_status: undefined,
1710
2003
  bucket_data_source_ids: [],
1711
2004
  parameter_lookup_source_ids: [],
2005
+ event_definition_ids: [],
1712
2006
  latest_pending_delete: 9n
1713
2007
  },
1714
2008
  {
@@ -1722,6 +2016,7 @@ streams:
1722
2016
  snapshot_status: undefined,
1723
2017
  bucket_data_source_ids: [],
1724
2018
  parameter_lookup_source_ids: [],
2019
+ event_definition_ids: [],
1725
2020
  latest_pending_delete: 12n
1726
2021
  }
1727
2022
  ]);
@@ -1810,7 +2105,7 @@ describe('sync - mongodb', () => {
1810
2105
  describe(`storage v${storageVersion}`, () => {
1811
2106
  registerSyncStorageTests(INITIALIZED_MONGO_STORAGE_FACTORY, storageVersion);
1812
2107
 
1813
- describe.runIf(storageVersion == 3)('V3 read filtering boundaries', () => {
2108
+ describe.runIf(storageVersion >= 3)('V3 read filtering boundaries', () => {
1814
2109
  async function setupFilteringTest() {
1815
2110
  await using factory = await INITIALIZED_MONGO_STORAGE_FACTORY.factory();
1816
2111
  const syncRules = await factory.updateSyncRules(
package/test/src/util.ts CHANGED
@@ -13,4 +13,4 @@ export const INITIALIZED_MONGO_REPORT_STORAGE_FACTORY = mongoTestReportStorageFa
13
13
  isCI: env.CI
14
14
  });
15
15
 
16
- export const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS;
16
+ export const TEST_STORAGE_VERSIONS = SUPPORTED_STORAGE_VERSIONS.filter((v) => v != 3);