@powersync/service-module-mongodb-storage 0.17.0 → 0.18.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 (185) hide show
  1. package/CHANGELOG.md +19 -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 +121 -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 +152 -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 +1039 -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
@@ -3,39 +3,86 @@ import { ReplicationAssertionError } from '@powersync/lib-services-framework';
3
3
  import { storage } from '@powersync/service-core';
4
4
  import * as bson from 'bson';
5
5
  import { mongoTableId } from '../../../utils/util.js';
6
- import { calculateCheckpointState } from '../CheckpointState.js';
6
+ import { canCheckpointState } from '../CheckpointState.js';
7
7
  import { MongoBucketBatch } from '../MongoBucketBatch.js';
8
8
  import { syncRuleStateUpdatePipeline } from '../SyncRuleStateUpdate.js';
9
9
  import { PersistedBatchV3 } from './PersistedBatchV3.js';
10
10
  import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
11
- function sameStringArray(left, right) {
12
- return left.length == right.length && left.every((value, index) => value == right[index]);
13
- }
11
+ import { createNewSourceTable, designateEventCarrier, overlappingSourceTableFilter, planSourceTableReconciliation, sourceTableDesiredResolution, sourceTableFromDocument } from './source-table-utils.js';
14
12
  export class MongoBucketBatchV3 extends MongoBucketBatch {
15
13
  store;
16
- syncConfigId;
14
+ syncConfigIds;
17
15
  needsActivationV3 = true;
18
16
  lastWaitingLogThrottledV3 = 0;
19
17
  constructor(options) {
20
18
  super(options);
21
- if (options.syncConfigId == null) {
19
+ const syncConfigIds = options.syncConfigIds ?? [];
20
+ if (syncConfigIds.length == 0) {
22
21
  throw new ReplicationAssertionError('Missing sync config id for v3 batch');
23
22
  }
24
- this.syncConfigId = options.syncConfigId;
25
- this.store = new SourceRecordStoreV3(this.db, this.group_id, this.mapping);
23
+ this.syncConfigIds = syncConfigIds;
24
+ this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
26
25
  }
27
26
  createPersistedBatch(writtenSize) {
28
- return new PersistedBatchV3(this.db, this.group_id, this.mapping, writtenSize, {
27
+ return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
29
28
  logger: this.logger
30
29
  });
31
30
  }
32
31
  get sourceRecordStore() {
33
32
  return this.store;
34
33
  }
34
+ async onReplicationTransactionFlush(session, lastOp) {
35
+ // Durably advance the stream-level head of persisted ops within the flush transaction.
36
+ // This ensures a checkpoint created later (even by an empty commit, or by a freshly-appended
37
+ // config that replicates nothing) covers all ops persisted before a potential crash.
38
+ await this.db.sync_rules.updateOne({
39
+ _id: this.replicationStreamId
40
+ }, {
41
+ $max: {
42
+ last_persisted_op: lastOp
43
+ }
44
+ }, { session });
45
+ }
46
+ selectedSyncConfigObjectIds(syncConfigIds) {
47
+ const selectedIds = new Set(this.syncConfigIds.map((id) => id.toHexString()));
48
+ return syncConfigIds
49
+ .filter((id) => selectedIds.has(id) && bson.ObjectId.isValid(id))
50
+ .map((id) => new bson.ObjectId(id));
51
+ }
52
+ relevantSyncConfigIds(table) {
53
+ const bucketDataSourceIds = table.bucketDataSources.map((source) => this.mapping.bucketSourceId(source));
54
+ const parameterLookupSourceIds = table.parameterLookupSources.map((source) => this.mapping.parameterLookupId(source));
55
+ return this.selectedSyncConfigObjectIds(this.mapping.syncConfigIdsForSourceTable(this.syncConfigIds.map((id) => id.toHexString()), table.ref, bucketDataSourceIds, parameterLookupSourceIds));
56
+ }
57
+ relevantSyncConfigIdsForTables(tables) {
58
+ const ids = new Map();
59
+ for (const table of tables) {
60
+ for (const id of this.relevantSyncConfigIds(table)) {
61
+ ids.set(id.toHexString(), id);
62
+ }
63
+ }
64
+ return [...ids.values()];
65
+ }
66
+ snapshotBlockingSourceTablesFilter() {
67
+ const clauses = this.syncConfigIds.flatMap((syncConfigId) => {
68
+ const filter = this.mapping.snapshotBlockingSourceTablesFilter(syncConfigId.toHexString());
69
+ return filter.$or ?? [];
70
+ });
71
+ if (clauses.length == 0) {
72
+ return {
73
+ snapshot_done: false,
74
+ _id: { $exists: false }
75
+ };
76
+ }
77
+ return {
78
+ snapshot_done: false,
79
+ $or: clauses
80
+ };
81
+ }
35
82
  async cleanupDroppedSourceTables(sourceTables) {
36
83
  for (const table of sourceTables) {
37
84
  await this.db
38
- .sourceRecordsV3(this.group_id, mongoTableId(table.id))
85
+ .sourceRecords(this.replicationStreamId, mongoTableId(table.id))
39
86
  .drop()
40
87
  .catch((error) => {
41
88
  if (lib_mongo.isMongoServerError(error) && error.codeName === 'NamespaceNotFound') {
@@ -46,178 +93,86 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
46
93
  }
47
94
  }
48
95
  async resolveTables(options) {
49
- const ref = options.source;
50
- const syncRules = options.syncRules ?? this.sync_rules;
51
- const matchingSources = syncRules.getMatchingSources(ref);
96
+ // The test-only override is a whole parsed set, so the sync rules and the mapping
97
+ // used below always come from the same parse.
98
+ const parsedOverride = options.parsedSyncConfig;
99
+ const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
100
+ const mapping = parsedOverride?.mapping ?? this.mapping;
52
101
  const { connection_id, source } = options;
53
- const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
54
- const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
55
- name: column.name,
56
- type: column.type,
57
- type_oid: column.typeId
58
- }));
102
+ const context = {
103
+ connectionId: connection_id,
104
+ connectionTag: source.connectionTag,
105
+ identity: {
106
+ schema: source.schema,
107
+ name: source.name,
108
+ objectId: source.objectId,
109
+ replicaIdColumns: source.replicaIdColumns.map((column) => ({
110
+ name: column.name,
111
+ type: column.type,
112
+ type_oid: column.typeId
113
+ }))
114
+ },
115
+ storeCurrentData: source.sendsCompleteRows !== true,
116
+ syncConfig,
117
+ mapping,
118
+ desired: sourceTableDesiredResolution(syncConfig, source, mapping)
119
+ };
59
120
  let result = null;
60
- const initializeSourceRecordsFor = [];
61
- await this.db.client.withSession(async (session) => {
62
- const col = this.db.commonSourceTables(this.group_id);
63
- const exactFilter = {
64
- connection_id,
65
- schema_name: schema,
66
- table_name: name,
67
- replica_id_columns2: normalizedReplicaIdColumns
68
- };
69
- if (objectId != null) {
70
- exactFilter.relation_id = objectId;
71
- }
72
- const exactDocs = (await col.find(exactFilter, { session }).toArray());
73
- const bucketSourceById = new Map(matchingSources.bucketDataSources.map((source) => [this.mapping.bucketSourceId(source), source]));
74
- const parameterLookupSourceById = new Map(matchingSources.parameterLookupSources.map((source) => [this.mapping.parameterLookupId(source), source]));
75
- const desiredBucketIds = new Set(bucketSourceById.keys());
76
- const desiredLookupIds = new Set(parameterLookupSourceById.keys());
77
- const desiredHasMembership = desiredBucketIds.size > 0 || desiredLookupIds.size > 0;
78
- const triggersEvent = syncRules.tableTriggersEvent(ref);
79
- const coveredBucketIds = new Set();
80
- const coveredLookupIds = new Set();
81
- const retainedDocIds = [];
82
- const tables = [];
83
- let retainedEventOnlyTable = false;
84
- for (const doc of exactDocs) {
85
- const bucketDataSourceIds = doc.bucket_data_source_ids.filter((id) => desiredBucketIds.has(id));
86
- const parameterLookupSourceIds = doc.parameter_lookup_source_ids.filter((id) => desiredLookupIds.has(id));
87
- const coversDesiredMembership = bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0;
88
- const coversEventOnlyTable = !desiredHasMembership && triggersEvent && !retainedEventOnlyTable;
89
- for (const id of bucketDataSourceIds) {
90
- coveredBucketIds.add(id);
91
- }
92
- for (const id of parameterLookupSourceIds) {
93
- coveredLookupIds.add(id);
94
- }
95
- const updates = {};
96
- if (!sameStringArray(doc.bucket_data_source_ids, bucketDataSourceIds) ||
97
- !sameStringArray(doc.parameter_lookup_source_ids, parameterLookupSourceIds)) {
98
- updates.bucket_data_source_ids = bucketDataSourceIds;
99
- updates.parameter_lookup_source_ids = parameterLookupSourceIds;
100
- }
101
- if (Object.keys(updates).length > 0) {
102
- await col.updateOne({ _id: doc._id }, { $set: updates }, { session });
103
- }
104
- if (coversDesiredMembership || coversEventOnlyTable) {
105
- if (coversEventOnlyTable) {
106
- retainedEventOnlyTable = true;
121
+ const session = this.db.client.startSession();
122
+ await using _ = { [Symbol.asyncDispose]: () => session.endSession() };
123
+ await session.withTransaction(async () => {
124
+ const col = this.db.sourceTables(this.replicationStreamId);
125
+ // Fetch every persisted source-table doc that can overlap this physical table.
126
+ // Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
127
+ const candidateDocs = await col
128
+ .find(overlappingSourceTableFilter(connection_id, context.identity), { session })
129
+ .toArray();
130
+ // Pure planning: which docs to retain (and narrow), whether a new doc is needed for
131
+ // uncovered memberships, and which docs conflict with the current identity.
132
+ const plan = planSourceTableReconciliation(candidateDocs, context);
133
+ // Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
134
+ // coverage ids so compatible future configs can reuse already-snapshotted data.
135
+ // Narrowing occurs after removing a sync config, meaning we don't process those
136
+ // definitions anymore.
137
+ for (const update of plan.narrowingUpdates) {
138
+ await col.updateOne({ _id: update.id }, {
139
+ $set: {
140
+ bucket_data_source_ids: update.memberships.bucketDataSourceIds,
141
+ parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
107
142
  }
108
- retainedDocIds.push(doc._id);
109
- const table = this.sourceTableFromDocument({
110
- ...doc,
111
- bucket_data_source_ids: bucketDataSourceIds,
112
- parameter_lookup_source_ids: parameterLookupSourceIds
113
- }, connectionTag, syncRules, {
114
- bucketDataSources: bucketDataSourceIds.map((id) => bucketSourceById.get(id)),
115
- parameterLookupSources: parameterLookupSourceIds.map((id) => parameterLookupSourceById.get(id))
116
- });
117
- table.storeCurrentData = sendsCompleteRows !== true;
118
- tables.push(table);
119
- }
143
+ }, { session });
120
144
  }
121
- const uncoveredBucketIds = [...desiredBucketIds].filter((id) => !coveredBucketIds.has(id));
122
- const uncoveredLookupIds = [...desiredLookupIds].filter((id) => !coveredLookupIds.has(id));
123
- if (uncoveredBucketIds.length > 0 || uncoveredLookupIds.length > 0 || (triggersEvent && tables.length == 0)) {
145
+ // Any desired membership not covered by an existing doc gets a new source table.
146
+ // That table snapshots only the uncovered memberships.
147
+ if (plan.newTableMemberships != null) {
124
148
  const id = options.idGenerator ? options.idGenerator() : new bson.ObjectId();
125
- const sourceTable = new storage.SourceTable({
126
- id,
127
- ref,
128
- objectId,
129
- replicaIdColumns,
130
- snapshotComplete: false,
131
- bucketDataSources: uncoveredBucketIds.map((id) => bucketSourceById.get(id)),
132
- parameterLookupSources: uncoveredLookupIds.map((id) => parameterLookupSourceById.get(id))
133
- });
134
- sourceTable.syncData = uncoveredBucketIds.length > 0;
135
- sourceTable.syncParameters = uncoveredLookupIds.length > 0;
136
- sourceTable.syncEvent = triggersEvent;
137
- sourceTable.storeCurrentData = sendsCompleteRows !== true;
138
- const createDoc = {
139
- _id: id,
140
- connection_id,
141
- relation_id: objectId,
142
- schema_name: schema,
143
- table_name: name,
144
- replica_id_columns: null,
145
- replica_id_columns2: normalizedReplicaIdColumns,
146
- snapshot_done: false,
147
- snapshot_status: undefined,
148
- bucket_data_source_ids: uncoveredBucketIds,
149
- parameter_lookup_source_ids: uncoveredLookupIds
150
- };
151
- await col.insertOne(createDoc, { session });
152
- initializeSourceRecordsFor.push(createDoc._id);
153
- retainedDocIds.push(createDoc._id);
154
- tables.push(sourceTable);
155
- }
156
- const conflictFilter = [{ schema_name: schema, table_name: name }];
157
- if (objectId != null) {
158
- conflictFilter.push({ relation_id: objectId });
149
+ const { doc, table } = createNewSourceTable(id, plan.newTableMemberships, context);
150
+ await col.insertOne(doc, { session });
151
+ await this.db.initializeSourceRecordsCollection(this.replicationStreamId, doc._id, session);
152
+ plan.tables.push(table);
159
153
  }
160
- const dropTables = await col
161
- .find({
162
- connection_id,
163
- _id: { $nin: retainedDocIds },
164
- $or: conflictFilter
165
- }, { session })
166
- .toArray();
154
+ // If memberships are split across multiple source tables, only one may fire events.
155
+ designateEventCarrier(plan.tables, context.desired.triggersEvent);
167
156
  result = {
168
- tables,
169
- dropTables: dropTables.map((doc) => this.sourceTableFromDocument(doc, connectionTag, syncRules))
157
+ tables: plan.tables,
158
+ dropTables: plan.dropDocs.map((doc) => sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping))
170
159
  };
171
160
  });
172
- for (const sourceTableId of initializeSourceRecordsFor) {
173
- await this.db.initializeSourceRecordsCollection(this.group_id, sourceTableId);
174
- }
175
161
  return result;
176
162
  }
177
- sourceTableFromDocument(doc, connectionTag, syncRules, memberships) {
178
- const resolvedMemberships = memberships ?? this.sourceTableMembershipsFromDocument(doc, syncRules);
179
- const table = new storage.SourceTable({
180
- id: doc._id,
181
- ref: {
182
- connectionTag,
183
- schema: doc.schema_name,
184
- name: doc.table_name
185
- },
186
- objectId: doc.relation_id,
187
- replicaIdColumns: doc.replica_id_columns2.map((c) => ({ name: c.name, typeId: c.type_oid, type: c.type })),
188
- snapshotComplete: doc.snapshot_done ?? true,
189
- bucketDataSources: resolvedMemberships.bucketDataSources,
190
- parameterLookupSources: resolvedMemberships.parameterLookupSources
191
- });
192
- table.syncData = table.bucketDataSources.length > 0;
193
- table.syncParameters = table.parameterLookupSources.length > 0;
194
- table.syncEvent = syncRules.tableTriggersEvent(table.ref);
195
- table.snapshotStatus =
196
- doc.snapshot_status == null
197
- ? undefined
198
- : {
199
- lastKey: doc.snapshot_status.last_key?.buffer ?? null,
200
- totalEstimatedCount: doc.snapshot_status.total_estimated_count,
201
- replicatedCount: doc.snapshot_status.replicated_count
202
- };
203
- return table;
204
- }
205
- sourceTableMembershipsFromDocument(doc, syncRules) {
206
- const bucketDataSourceIds = new Set(doc.bucket_data_source_ids);
207
- const parameterLookupSourceIds = new Set(doc.parameter_lookup_source_ids);
208
- return {
209
- bucketDataSources: syncRules.bucketDataSources.filter((source) => bucketDataSourceIds.has(this.mapping.bucketSourceId(source))),
210
- parameterLookupSources: syncRules.bucketParameterLookupSources.filter((source) => parameterLookupSourceIds.has(this.mapping.parameterLookupId(source)))
211
- };
212
- }
213
163
  async getSourceTableStatus(table) {
214
164
  const doc = (await this.db
215
- .commonSourceTables(this.group_id)
165
+ .sourceTables(this.replicationStreamId)
216
166
  .findOne({ _id: mongoTableId(table.id) }, { session: this.session }));
217
167
  if (doc == null) {
218
168
  return null;
219
169
  }
220
- return this.sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules);
170
+ const refreshed = sourceTableFromDocument(doc, table.ref.connectionTag, this.sync_rules, this.mapping);
171
+ // The event-carrier designation is decided per resolveTables result and not persisted -
172
+ // preserve the caller's designation instead of recomputing it from the ref, so that
173
+ // refreshing a non-carrier table does not make it fire events.
174
+ refreshed.syncEvent = table.syncEvent;
175
+ return refreshed;
221
176
  }
222
177
  async commit(lsn, options) {
223
178
  const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
@@ -233,80 +188,125 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
233
188
  }, {
234
189
  session: this.session
235
190
  });
236
- const preUpdateDocument = await this.db.sync_rules.findOne({
237
- _id: this.group_id,
238
- 'sync_configs._id': this.syncConfigId
191
+ const preUpdateDocument = (await this.db.sync_rules.findOne({
192
+ _id: this.replicationStreamId,
193
+ 'sync_configs._id': { $in: this.syncConfigIds }
239
194
  }, {
240
195
  session: this.session,
241
196
  projection: {
242
- snapshot_lsn: 1,
243
- sync_configs: {
244
- $elemMatch: {
245
- _id: this.syncConfigId
246
- }
247
- }
197
+ sync_configs: 1,
198
+ last_persisted_op: 1
248
199
  }
249
- });
250
- const state = preUpdateDocument?.sync_configs?.[0];
251
- if (state == null) {
252
- throw new ReplicationAssertionError(`Failed to update checkpoint - no matching sync_config for _id: ${this.group_id}/${this.syncConfigId.toHexString()}`);
200
+ }));
201
+ const states = preUpdateDocument?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
202
+ if (states.length == 0) {
203
+ throw new ReplicationAssertionError(`Failed to update checkpoint - no matching sync_config for _id: ${this.replicationStreamId}/${this.syncConfigIds
204
+ .map((id) => id.toHexString())
205
+ .join(',')}`);
253
206
  }
254
- const checkpointState = calculateCheckpointState({
255
- lsn,
256
- snapshotDone: state.snapshot_done === true,
257
- lastCheckpointLsn: state.last_checkpoint_lsn,
258
- noCheckpointBefore: state.no_checkpoint_before,
259
- keepaliveOp: state.keepalive_op == null ? null : BigInt(state.keepalive_op),
260
- lastCheckpoint: state.last_checkpoint,
261
- persistedOp: this.persisted_op,
262
- createEmptyCheckpoints
263
- });
264
- const updateSet = {
265
- last_keepalive_ts: now,
266
- last_fatal_error: null,
267
- last_fatal_error_ts: null,
268
- 'sync_configs.$[config].keepalive_op': checkpointState.newKeepaliveOp,
269
- 'sync_configs.$[config].last_checkpoint': checkpointState.newLastCheckpoint
270
- };
271
- if (checkpointState.checkpointCreated) {
272
- updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
273
- updateSet['snapshot_lsn'] = null;
274
- updateSet['last_checkpoint_ts'] = now;
207
+ // The replication job / MongoBucketBatch must be constructed with all replicating (PROCESSING / ACTIVE)
208
+ // sync configs in the stream, otherwise we'll get inconsistencies. Configs in other states (e.g. STOP)
209
+ // remain embedded in the document and are ignored here.
210
+ const missingSyncConfig = preUpdateDocument.sync_configs.find((config) => [storage.SyncRuleState.PROCESSING, storage.SyncRuleState.ACTIVE].includes(config.state) &&
211
+ !this.syncConfigIds.some((id) => id.equals(config._id)));
212
+ if (missingSyncConfig != null) {
213
+ throw new ReplicationAssertionError(`Replication job not configured for sync config ${missingSyncConfig._id}`);
275
214
  }
276
- await this.db.sync_rules.updateOne({
277
- _id: this.group_id,
278
- 'sync_configs._id': this.syncConfigId
279
- }, {
280
- $set: updateSet
281
- }, {
282
- session: this.session,
283
- arrayFilters: [{ 'config._id': this.syncConfigId }]
284
- });
285
- if (checkpointState.checkpointBlocked) {
286
- if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
287
- this.logger.info(`Waiting before creating checkpoint, currently at ${lsn} / ${checkpointState.newKeepaliveOp}. Current state: ${JSON.stringify({
215
+ // Effective head of the stream's op sequence.
216
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and this
217
+ // read uses the same session, so it covers all ops persisted by this batch.
218
+ const newCheckpoint = preUpdateDocument?.last_persisted_op == null ? 0n : BigInt(preUpdateDocument.last_persisted_op);
219
+ let checkpointBlocked = false;
220
+ let checkpointCreated = false;
221
+ let checkpointLogState = null;
222
+ const unblockedConfigIds = [];
223
+ for (const state of states) {
224
+ if (state.last_checkpoint != null && state.last_checkpoint > newCheckpoint) {
225
+ // last_persisted_op is $max-advanced durably in the same transaction as every flush, and
226
+ // checkpoints are only ever created at that head, so a checkpoint past the head means the
227
+ // op sequence or the stored state is corrupt.
228
+ throw new ReplicationAssertionError(`Invariant violation: sync config ${state._id} has last_checkpoint ${state.last_checkpoint} > stream head ${newCheckpoint}`);
229
+ }
230
+ const canCheckpoint = canCheckpointState(lsn, {
231
+ snapshotDone: state.snapshot_done === true,
232
+ lastCheckpointLsn: state.last_checkpoint_lsn,
233
+ noCheckpointBefore: state.no_checkpoint_before
234
+ });
235
+ if (!canCheckpoint) {
236
+ checkpointBlocked = true;
237
+ // Log the first blocked config's state.
238
+ checkpointLogState ??= {
288
239
  snapshot_done: state.snapshot_done,
289
240
  last_checkpoint_lsn: state.last_checkpoint_lsn,
290
241
  no_checkpoint_before: state.no_checkpoint_before
291
- })}`);
242
+ };
243
+ continue;
244
+ }
245
+ checkpointCreated ||= createEmptyCheckpoints || state.last_checkpoint !== newCheckpoint;
246
+ unblockedConfigIds.push(state._id);
247
+ }
248
+ // Every commit advances the stream's resume position: commit() flushes first, so all
249
+ // source changes up to this lsn have been persisted, even when checkpoints are blocked.
250
+ // In the future we could also advance this on flush, when the connector provides the
251
+ // current position (see setResumeLsn, which connectors may already call after flushing).
252
+ const resumeLsnUpdate = { resume_lsn: lsn };
253
+ if (unblockedConfigIds.length > 0) {
254
+ // All unblocked configs get the SAME new value, so we apply it with a single updateOne
255
+ // (single-document atomicity).
256
+ const updateSet = {
257
+ last_keepalive_ts: now,
258
+ last_fatal_error: null,
259
+ last_fatal_error_ts: null
260
+ };
261
+ // Only advance checkpoint fields when an actual (non-empty) checkpoint is created, matching
262
+ // the previous per-config / v1 behaviour.
263
+ if (checkpointCreated) {
264
+ updateSet['sync_configs.$[config].last_checkpoint'] = newCheckpoint;
265
+ updateSet['sync_configs.$[config].last_checkpoint_lsn'] = lsn;
266
+ updateSet['last_checkpoint_ts'] = now;
267
+ }
268
+ await this.db.sync_rules.updateOne({
269
+ _id: this.replicationStreamId,
270
+ 'sync_configs._id': { $in: unblockedConfigIds }
271
+ }, { $set: updateSet, $max: resumeLsnUpdate }, {
272
+ session: this.session,
273
+ arrayFilters: checkpointCreated ? [{ 'config._id': { $in: unblockedConfigIds } }] : undefined
274
+ });
275
+ }
276
+ else {
277
+ // All selected configs are blocked - only update keepalive/error tracking and the
278
+ // resume position.
279
+ await this.db.sync_rules.updateOne({
280
+ _id: this.replicationStreamId
281
+ }, {
282
+ $set: {
283
+ last_keepalive_ts: now,
284
+ last_fatal_error: null,
285
+ last_fatal_error_ts: null
286
+ },
287
+ $max: resumeLsnUpdate
288
+ }, { session: this.session });
289
+ }
290
+ if (checkpointBlocked) {
291
+ if (Date.now() - this.lastWaitingLogThrottledV3 > 5_000) {
292
+ this.logger.info(`Waiting before creating checkpoint, currently at ${lsn}. Current state: ${JSON.stringify(checkpointLogState)}`);
292
293
  this.lastWaitingLogThrottledV3 = Date.now();
293
294
  }
294
295
  }
295
296
  else {
296
- if (checkpointState.checkpointCreated) {
297
- this.logger.debug(`Created checkpoint at ${lsn} / ${checkpointState.newLastCheckpoint}`);
297
+ if (checkpointCreated) {
298
+ this.logger.debug(`Created checkpoint at ${lsn}`);
298
299
  }
299
300
  await this.autoActivateV3(lsn);
301
+ // All configs are now checkpointed at newCheckpoint (the stream head).
302
+ await this.sourceRecordStore.postCommitCleanup(newCheckpoint, this.logger);
303
+ }
304
+ if (checkpointCreated) {
300
305
  await this.db.notifyCheckpoint();
301
- this.persisted_op = null;
302
- this.last_checkpoint_lsn = lsn;
303
- if (checkpointState.newLastCheckpoint != null) {
304
- await this.sourceRecordStore.postCommitCleanup(checkpointState.newLastCheckpoint, this.logger);
305
- }
306
306
  }
307
307
  return {
308
- checkpointBlocked: checkpointState.checkpointBlocked,
309
- checkpointCreated: checkpointState.checkpointCreated
308
+ checkpointBlocked,
309
+ checkpointCreated
310
310
  };
311
311
  }
312
312
  async keepalive(lsn) {
@@ -314,11 +314,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
314
314
  }
315
315
  async setResumeLsn(lsn) {
316
316
  await this.db.sync_rules.updateOne({
317
- _id: this.group_id,
318
- 'sync_configs._id': this.syncConfigId
317
+ _id: this.replicationStreamId
319
318
  }, {
320
319
  $set: {
321
- snapshot_lsn: lsn
320
+ resume_lsn: lsn
322
321
  }
323
322
  }, { session: this.session });
324
323
  }
@@ -330,28 +329,26 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
330
329
  let activated = false;
331
330
  await session.withTransaction(async () => {
332
331
  const doc = await this.db.sync_rules.findOne({
333
- _id: this.group_id,
334
- 'sync_configs._id': this.syncConfigId
332
+ _id: this.replicationStreamId,
333
+ 'sync_configs._id': { $in: this.syncConfigIds }
335
334
  }, {
336
335
  session,
337
336
  projection: {
338
337
  state: 1,
339
- sync_configs: {
340
- $elemMatch: {
341
- _id: this.syncConfigId
342
- }
343
- }
338
+ sync_configs: 1
344
339
  }
345
340
  });
346
- const state = doc?.sync_configs?.[0];
347
- if (doc &&
348
- doc.state == storage.SyncRuleState.PROCESSING &&
349
- state?.state == storage.SyncRuleState.PROCESSING &&
350
- state.snapshot_done &&
351
- state.last_checkpoint != null) {
341
+ const states = doc?.sync_configs?.filter((config) => this.syncConfigIds.some((id) => id.equals(config._id))) ?? [];
342
+ if (doc == null || states.length == 0) {
343
+ return;
344
+ }
345
+ const processingStates = states.filter((state) => state.state == storage.SyncRuleState.PROCESSING);
346
+ if (doc.state == storage.SyncRuleState.PROCESSING &&
347
+ processingStates.length == states.length &&
348
+ states.every((state) => state.snapshot_done && state.last_checkpoint != null)) {
352
349
  await this.db.sync_rules.updateOne({
353
- _id: this.group_id,
354
- 'sync_configs._id': this.syncConfigId
350
+ _id: this.replicationStreamId,
351
+ 'sync_configs._id': { $in: this.syncConfigIds }
355
352
  }, {
356
353
  $set: {
357
354
  state: storage.SyncRuleState.ACTIVE,
@@ -359,15 +356,35 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
359
356
  }
360
357
  }, {
361
358
  session,
362
- arrayFilters: [{ 'config._id': this.syncConfigId }]
359
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
363
360
  });
364
361
  await this.db.sync_rules.updateMany({
365
- _id: { $ne: this.group_id },
362
+ _id: { $ne: this.replicationStreamId },
366
363
  state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
367
364
  }, syncRuleStateUpdatePipeline(storage.SyncRuleState.STOP), { session });
368
365
  activated = true;
369
366
  }
370
- else if (doc?.state != storage.SyncRuleState.PROCESSING) {
367
+ else if (doc.state == storage.SyncRuleState.ACTIVE &&
368
+ processingStates.length > 0 &&
369
+ processingStates.every((state) => state.snapshot_done && state.last_checkpoint != null)) {
370
+ await this.db.sync_rules.updateOne({
371
+ _id: this.replicationStreamId,
372
+ 'sync_configs._id': { $in: processingStates.map((state) => state._id) }
373
+ }, {
374
+ $set: {
375
+ 'sync_configs.$[activeConfig].state': storage.SyncRuleState.STOP,
376
+ 'sync_configs.$[processingConfig].state': storage.SyncRuleState.ACTIVE
377
+ }
378
+ }, {
379
+ session,
380
+ arrayFilters: [
381
+ { 'activeConfig.state': storage.SyncRuleState.ACTIVE },
382
+ { 'processingConfig._id': { $in: processingStates.map((state) => state._id) } }
383
+ ]
384
+ });
385
+ activated = true;
386
+ }
387
+ else if (doc.state != storage.SyncRuleState.PROCESSING && doc.state != storage.SyncRuleState.ACTIVE) {
371
388
  this.needsActivationV3 = false;
372
389
  }
373
390
  });
@@ -379,8 +396,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
379
396
  }
380
397
  async markAllSnapshotDone(no_checkpoint_before_lsn) {
381
398
  await this.db.sync_rules.updateOne({
382
- _id: this.group_id,
383
- 'sync_configs._id': this.syncConfigId
399
+ _id: this.replicationStreamId,
400
+ 'sync_configs._id': { $in: this.syncConfigIds }
384
401
  }, {
385
402
  $set: {
386
403
  'sync_configs.$[config].snapshot_done': true,
@@ -391,18 +408,22 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
391
408
  }
392
409
  }, {
393
410
  session: this.session,
394
- arrayFilters: [{ 'config._id': this.syncConfigId }]
411
+ arrayFilters: [{ 'config._id': { $in: this.syncConfigIds } }]
395
412
  });
396
413
  }
397
414
  async markSnapshotDone(no_checkpoint_before_lsn, options) {
398
415
  await this.withTransaction(async () => {
399
416
  // Protect against race conditions
400
- const count = await this.db.sourceTablesV3(this.group_id).countDocuments({
401
- snapshot_done: false
402
- }, { session: this.session });
403
- if (count > 0) {
417
+ const blockingTables = await this.db
418
+ .sourceTables(this.replicationStreamId)
419
+ .find(this.snapshotBlockingSourceTablesFilter(), {
420
+ session: this.session,
421
+ projection: { schema_name: 1, table_name: 1 }
422
+ })
423
+ .toArray();
424
+ if (blockingTables.length > 0) {
404
425
  if (options?.throwOnConflict ?? true) {
405
- throw new ReplicationAssertionError(`Cannot mark snapshot done while ${count} source table${count == 1 ? '' : 's'} still require snapshotting`);
426
+ throw new ReplicationAssertionError(`Cannot mark snapshot done while source tables still require snapshotting. Tables: ${blockingTables.map((t) => `${t.schema_name}.${t.table_name}`).join(', ')}`);
406
427
  }
407
428
  else {
408
429
  return;
@@ -411,24 +432,29 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
411
432
  await this.markAllSnapshotDone(no_checkpoint_before_lsn);
412
433
  });
413
434
  }
414
- async markTableSnapshotRequired(_table) {
435
+ async markTableSnapshotRequired(table) {
436
+ const syncConfigIds = this.relevantSyncConfigIds(table);
437
+ if (syncConfigIds.length == 0) {
438
+ return;
439
+ }
415
440
  await this.db.sync_rules.updateOne({
416
- _id: this.group_id,
417
- 'sync_configs._id': this.syncConfigId
441
+ _id: this.replicationStreamId,
442
+ 'sync_configs._id': { $in: syncConfigIds }
418
443
  }, {
419
444
  $set: {
420
445
  'sync_configs.$[config].snapshot_done': false
421
446
  }
422
447
  }, {
423
448
  session: this.session,
424
- arrayFilters: [{ 'config._id': this.syncConfigId }]
449
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
425
450
  });
426
451
  }
427
452
  async markTableSnapshotDone(tables, no_checkpoint_before_lsn) {
428
453
  const session = this.session;
429
454
  const ids = tables.map((table) => mongoTableId(table.id));
455
+ const syncConfigIds = this.relevantSyncConfigIdsForTables(tables);
430
456
  await this.withTransaction(async () => {
431
- await this.db.commonSourceTables(this.group_id).updateMany({ _id: { $in: ids } }, {
457
+ await this.db.sourceTables(this.replicationStreamId).updateMany({ _id: { $in: ids } }, {
432
458
  $set: {
433
459
  snapshot_done: true
434
460
  },
@@ -436,10 +462,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
436
462
  snapshot_status: 1
437
463
  }
438
464
  }, { session });
439
- if (no_checkpoint_before_lsn != null) {
465
+ if (no_checkpoint_before_lsn != null && syncConfigIds.length > 0) {
440
466
  await this.db.sync_rules.updateOne({
441
- _id: this.group_id,
442
- 'sync_configs._id': this.syncConfigId
467
+ _id: this.replicationStreamId,
468
+ 'sync_configs._id': { $in: syncConfigIds }
443
469
  }, {
444
470
  $set: {
445
471
  last_keepalive_ts: new Date()
@@ -449,7 +475,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
449
475
  }
450
476
  }, {
451
477
  session: this.session,
452
- arrayFilters: [{ 'config._id': this.syncConfigId }]
478
+ // Only set for sync configs that use this table
479
+ arrayFilters: [{ 'config._id': { $in: syncConfigIds } }]
453
480
  });
454
481
  }
455
482
  });