@powersync/service-module-mongodb-storage 0.15.3 → 0.16.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 (204) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
  3. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +3 -3
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
  7. package/dist/storage/MongoBucketStorage.d.ts +5 -3
  8. package/dist/storage/MongoBucketStorage.js +50 -36
  9. package/dist/storage/MongoBucketStorage.js.map +1 -1
  10. package/dist/storage/MongoReportStorage.js.map +1 -1
  11. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
  12. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  13. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  14. package/dist/storage/implementation/MongoBucketBatch.d.ts +16 -14
  15. package/dist/storage/implementation/MongoBucketBatch.js +80 -115
  16. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  17. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  18. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  19. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  20. package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
  21. package/dist/storage/implementation/MongoChecksums.js +13 -72
  22. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  23. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  24. package/dist/storage/implementation/MongoCompactor.js +229 -296
  25. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  26. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  27. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  28. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  29. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  35. package/dist/storage/implementation/MongoStorageProvider.js +1 -1
  36. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +49 -30
  38. package/dist/storage/implementation/MongoSyncBucketStorage.js +96 -388
  39. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  40. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
  41. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  42. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  43. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  44. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  45. package/dist/storage/implementation/OperationBatch.js +1 -1
  46. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  47. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  48. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  49. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  50. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  51. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  52. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  53. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  54. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  55. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  56. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  57. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  58. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  59. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  60. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  61. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  62. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  63. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  64. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  65. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  66. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  67. package/dist/storage/implementation/db.d.ts +34 -34
  68. package/dist/storage/implementation/db.js +78 -98
  69. package/dist/storage/implementation/db.js.map +1 -1
  70. package/dist/storage/implementation/models.d.ts +63 -34
  71. package/dist/storage/implementation/models.js +21 -2
  72. package/dist/storage/implementation/models.js.map +1 -1
  73. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  74. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  75. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  76. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  77. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  78. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  79. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  80. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  81. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  82. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  83. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  84. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  85. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  86. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
  87. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  88. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  89. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  90. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  91. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  92. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  93. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  94. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  95. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  96. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  97. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  98. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  99. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  100. package/dist/storage/implementation/v1/models.d.ts +34 -0
  101. package/dist/storage/implementation/v1/models.js +37 -0
  102. package/dist/storage/implementation/v1/models.js.map +1 -0
  103. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  104. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  105. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  106. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  107. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  108. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  109. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  110. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  111. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  112. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  113. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  114. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  115. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  116. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  117. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  118. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  119. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
  120. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  121. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  122. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  123. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  124. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  125. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  126. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  127. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  128. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  129. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  130. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  131. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  132. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  133. package/dist/storage/implementation/v3/models.d.ts +43 -0
  134. package/dist/storage/implementation/v3/models.js +34 -0
  135. package/dist/storage/implementation/v3/models.js.map +1 -0
  136. package/dist/storage/storage-index.d.ts +8 -5
  137. package/dist/storage/storage-index.js +8 -5
  138. package/dist/storage/storage-index.js.map +1 -1
  139. package/dist/utils/util.d.ts +11 -4
  140. package/dist/utils/util.js +25 -4
  141. package/dist/utils/util.js.map +1 -1
  142. package/package.json +9 -9
  143. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  144. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  145. package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
  146. package/src/storage/MongoBucketStorage.ts +97 -62
  147. package/src/storage/MongoReportStorage.ts +2 -2
  148. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  149. package/src/storage/implementation/MongoBucketBatch.ts +110 -144
  150. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  151. package/src/storage/implementation/MongoChecksums.ts +53 -76
  152. package/src/storage/implementation/MongoCompactor.ts +374 -404
  153. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  154. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  155. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +18 -1
  156. package/src/storage/implementation/MongoStorageProvider.ts +1 -1
  157. package/src/storage/implementation/MongoSyncBucketStorage.ts +190 -457
  158. package/src/storage/implementation/MongoSyncRulesLock.ts +12 -14
  159. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
  160. package/src/storage/implementation/OperationBatch.ts +1 -1
  161. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  162. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  163. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  164. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  165. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  166. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  167. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  168. package/src/storage/implementation/db.ts +107 -128
  169. package/src/storage/implementation/models.ts +84 -38
  170. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  171. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  172. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  173. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  174. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
  175. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  176. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  177. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  178. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  179. package/src/storage/implementation/v1/models.ts +84 -0
  180. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  181. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  182. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  183. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  184. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  185. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
  186. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  187. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  188. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  189. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  190. package/src/storage/implementation/v3/models.ts +96 -0
  191. package/src/storage/storage-index.ts +8 -5
  192. package/src/utils/util.ts +36 -7
  193. package/test/src/__snapshots__/storage_sync.test.ts.snap +282 -0
  194. package/test/src/connection-report-storage.test.ts +3 -3
  195. package/test/src/setup.ts +1 -1
  196. package/test/src/storage.test.ts +2 -2
  197. package/test/src/storage_compacting.test.ts +57 -29
  198. package/test/src/storage_sync.test.ts +351 -5
  199. package/test/tsconfig.json +0 -1
  200. package/tsconfig.tsbuildinfo +1 -1
  201. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  202. package/dist/storage/implementation/PersistedBatch.js +0 -354
  203. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  204. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -0,0 +1,72 @@
1
+ import { ServiceAssertionError } from '@powersync/lib-services-framework';
2
+ import { BucketDataSource, ParameterIndexLookupCreator, SyncConfigWithErrors } from '@powersync/service-sync-rules';
3
+ import { SyncRuleDocument } from './models.js';
4
+
5
+ export type BucketDefinitionId = string;
6
+ export type ParameterIndexId = string;
7
+
8
+ export class BucketDefinitionMapping {
9
+ static fromSyncRules(doc: Pick<SyncRuleDocument, 'rule_mapping'>): BucketDefinitionMapping {
10
+ return new BucketDefinitionMapping(doc.rule_mapping?.definitions ?? {}, doc.rule_mapping?.parameter_indexes ?? {});
11
+ }
12
+
13
+ static fromParsedSyncRules(syncRules: SyncConfigWithErrors): BucketDefinitionMapping {
14
+ const definitionNames = syncRules.config.bucketDataSources.map((source) => source.uniqueName).sort();
15
+ const parameterKeys = syncRules.config.bucketParameterLookupSources
16
+ .map((source) => `${source.defaultLookupScope.lookupName}#${source.defaultLookupScope.queryId}`)
17
+ .sort();
18
+
19
+ const definitions: Record<string, BucketDefinitionId> = {};
20
+ const parameterLookups: Record<string, ParameterIndexId> = {};
21
+
22
+ for (const [index, uniqueName] of definitionNames.entries()) {
23
+ definitions[uniqueName] = (index + 1).toString(16);
24
+ }
25
+ for (const [index, key] of parameterKeys.entries()) {
26
+ parameterLookups[key] = (index + 1).toString(16);
27
+ }
28
+
29
+ return new BucketDefinitionMapping(definitions, parameterLookups);
30
+ }
31
+
32
+ constructor(
33
+ private definitions: Record<string, BucketDefinitionId> = {},
34
+ private parameterLookupMapping: Record<string, ParameterIndexId> = {}
35
+ ) {}
36
+
37
+ bucketSourceId(source: BucketDataSource): BucketDefinitionId {
38
+ const defId = this.definitions[source.uniqueName];
39
+ if (defId == null) {
40
+ throw new ServiceAssertionError(`No mapping found for bucket source ${source.uniqueName}`);
41
+ }
42
+ return defId;
43
+ }
44
+
45
+ allBucketDefinitionIds(): BucketDefinitionId[] {
46
+ return Object.values(this.definitions);
47
+ }
48
+
49
+ allParameterIndexIds(): ParameterIndexId[] {
50
+ return Object.values(this.parameterLookupMapping);
51
+ }
52
+
53
+ parameterLookupId(source: ParameterIndexLookupCreator): ParameterIndexId {
54
+ const key = this.parameterLookupKey(source.defaultLookupScope.lookupName, source.defaultLookupScope.queryId);
55
+ const defId = this.parameterLookupMapping[key];
56
+ if (defId == null) {
57
+ throw new ServiceAssertionError(`No mapping found for parameter lookup source ${key}`);
58
+ }
59
+ return defId;
60
+ }
61
+
62
+ private parameterLookupKey(lookupName: string, queryId: string) {
63
+ return `${lookupName}#${queryId}`;
64
+ }
65
+
66
+ serialize(): NonNullable<SyncRuleDocument['rule_mapping']> {
67
+ return {
68
+ definitions: { ...this.definitions },
69
+ parameter_indexes: { ...this.parameterLookupMapping }
70
+ };
71
+ }
72
+ }
@@ -5,7 +5,6 @@ import * as bson from 'bson';
5
5
  import {
6
6
  BaseObserver,
7
7
  container,
8
- logger as defaultLogger,
9
8
  ErrorCode,
10
9
  errors,
11
10
  Logger,
@@ -18,24 +17,23 @@ import {
18
17
  deserializeBson,
19
18
  InternalOpId,
20
19
  isCompleteRow,
20
+ PerformanceTracer,
21
21
  SaveOperationTag,
22
22
  storage,
23
23
  SyncRuleState,
24
24
  utils
25
25
  } from '@powersync/service-core';
26
26
  import * as timers from 'node:timers/promises';
27
- import { idPrefixFilter, mongoTableId } from '../../utils/util.js';
28
- import { PowerSyncMongo, VersionedPowerSyncMongo } from './db.js';
29
- import { CurrentBucket, CurrentDataDocument, SourceKey, SyncRuleDocument } from './models.js';
27
+ import { mongoTableId } from '../../utils/util.js';
28
+ import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
29
+ import { PersistedBatch } from './common/PersistedBatch.js';
30
+ import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
31
+ import type { VersionedPowerSyncMongo } from './db.js';
32
+ import { SyncRuleDocument } from './models.js';
33
+ import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
30
34
  import { MongoIdSequence } from './MongoIdSequence.js';
31
35
  import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
32
- import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
33
- import { PersistedBatch } from './PersistedBatch.js';
34
-
35
- /**
36
- * 15MB
37
- */
38
- export const MAX_ROW_SIZE = 15 * 1024 * 1024;
36
+ import { OperationBatch, RecordOperation } from './OperationBatch.js';
39
37
 
40
38
  // Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
41
39
  // While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
@@ -44,8 +42,6 @@ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
44
42
  // In the future, we can investigate allowing multiple replication streams operating independently.
45
43
  const replicationMutex = new utils.Mutex();
46
44
 
47
- export const EMPTY_DATA = new bson.Binary(bson.serialize({}));
48
-
49
45
  export interface MongoBucketBatchOptions {
50
46
  db: VersionedPowerSyncMongo;
51
47
  syncRules: HydratedSyncRules;
@@ -55,6 +51,7 @@ export interface MongoBucketBatchOptions {
55
51
  keepaliveOp: InternalOpId | null;
56
52
  resumeFromLsn: string | null;
57
53
  storeCurrentData: boolean;
54
+ mapping: BucketDefinitionMapping;
58
55
  /**
59
56
  * Set to true for initial replication.
60
57
  */
@@ -62,31 +59,35 @@ export interface MongoBucketBatchOptions {
62
59
 
63
60
  markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
64
61
 
65
- logger?: Logger;
62
+ logger: Logger;
63
+ tracer?: PerformanceTracer<'storage' | 'evaluate'>;
66
64
  }
67
65
 
68
- export class MongoBucketBatch
66
+ export abstract class MongoBucketBatch
69
67
  extends BaseObserver<storage.BucketBatchStorageListener>
70
68
  implements storage.BucketStorageBatch
71
69
  {
72
- private logger: Logger;
70
+ protected logger: Logger;
73
71
 
74
72
  private readonly client: mongo.MongoClient;
75
73
  public readonly db: VersionedPowerSyncMongo;
76
74
  public readonly session: mongo.ClientSession;
77
75
  private readonly sync_rules: HydratedSyncRules;
78
76
 
79
- private readonly group_id: number;
77
+ protected readonly group_id: number;
80
78
 
81
79
  private readonly slot_name: string;
82
80
  private readonly storeCurrentData: boolean;
83
81
  private readonly skipExistingRows: boolean;
82
+ protected readonly mapping: BucketDefinitionMapping;
84
83
 
85
84
  private batch: OperationBatch | null = null;
86
85
  private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
87
86
  private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
88
87
  private clearedError = false;
89
88
 
89
+ private tracer: PerformanceTracer<'storage' | 'evaluate'>;
90
+
90
91
  /**
91
92
  * Last LSN received associated with a checkpoint.
92
93
  *
@@ -119,7 +120,7 @@ export class MongoBucketBatch
119
120
 
120
121
  constructor(options: MongoBucketBatchOptions) {
121
122
  super();
122
- this.logger = options.logger ?? defaultLogger;
123
+ this.logger = options.logger;
123
124
  this.client = options.db.client;
124
125
  this.db = options.db;
125
126
  this.group_id = options.groupId;
@@ -129,11 +130,13 @@ export class MongoBucketBatch
129
130
  this.slot_name = options.slotName;
130
131
  this.sync_rules = options.syncRules;
131
132
  this.storeCurrentData = options.storeCurrentData;
133
+ this.mapping = options.mapping;
132
134
  this.skipExistingRows = options.skipExistingRows;
133
135
  this.markRecordUnavailable = options.markRecordUnavailable;
134
136
  this.batch = new OperationBatch();
135
137
 
136
138
  this.persisted_op = options.keepaliveOp ?? null;
139
+ this.tracer = options.tracer ?? new PerformanceTracer('MongoDB storage');
137
140
  }
138
141
 
139
142
  addCustomWriteCheckpoint(checkpoint: storage.BatchedCustomWriteCheckpointOptions): void {
@@ -147,6 +150,12 @@ export class MongoBucketBatch
147
150
  return this.last_checkpoint_lsn;
148
151
  }
149
152
 
153
+ protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
154
+
155
+ protected abstract get sourceRecordStore(): SourceRecordStore;
156
+
157
+ protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
158
+
150
159
  async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
151
160
  let result: storage.FlushedResult | null = null;
152
161
  // One flush may be split over multiple transactions.
@@ -165,6 +174,8 @@ export class MongoBucketBatch
165
174
  let last_op: InternalOpId | null = null;
166
175
  let resumeBatch: OperationBatch | null = null;
167
176
 
177
+ using _ = this.tracer.span('storage', 'flush');
178
+
168
179
  await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
169
180
  if (batch != null) {
170
181
  resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
@@ -198,6 +209,7 @@ export class MongoBucketBatch
198
209
  options?: storage.BucketBatchCommitOptions
199
210
  ): Promise<OperationBatch | null> {
200
211
  let sizes: Map<string, number> | undefined = undefined;
212
+ using _ = this.tracer.span('storage', 'replicate_batch');
201
213
  if (this.storeCurrentData && !this.skipExistingRows) {
202
214
  // We skip this step if we don't store current_data, since the sizes will
203
215
  // always be small in that case.
@@ -212,33 +224,12 @@ export class MongoBucketBatch
212
224
  // (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
213
225
  // the order of processing, which then becomes really tricky to manage.
214
226
  // This now takes 2+ queries, but doesn't have any issues with order of operations.
215
- const sizeLookups: SourceKey[] = batch.batch.map((r) => {
216
- return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
217
- });
218
-
219
- sizes = new Map<string, number>();
227
+ const sizeLookups = batch.batch.map((r) => ({
228
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
229
+ replicaId: r.beforeId
230
+ }));
220
231
 
221
- const sizeCursor: mongo.AggregationCursor<{ _id: SourceKey; size: number }> =
222
- this.db.common_current_data.aggregate(
223
- [
224
- {
225
- $match: {
226
- _id: { $in: sizeLookups }
227
- }
228
- },
229
- {
230
- $project: {
231
- _id: 1,
232
- size: { $bsonSize: '$$ROOT' }
233
- }
234
- }
235
- ],
236
- { session }
237
- );
238
- for await (let doc of sizeCursor.stream()) {
239
- const key = cacheKey(doc._id.t, doc._id.k);
240
- sizes.set(key, doc.size);
241
- }
232
+ sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
242
233
  }
243
234
 
244
235
  // If set, we need to start a new transaction with this batch.
@@ -256,64 +247,65 @@ export class MongoBucketBatch
256
247
  }
257
248
  continue;
258
249
  }
259
- const lookups: SourceKey[] = b.map((r) => {
260
- return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
261
- });
262
- let current_data_lookup = new Map<string, CurrentDataDocument>();
263
- // With skipExistingRows, we only need to know whether or not the row exists.
264
- const projection = this.skipExistingRows ? { _id: 1 } : undefined;
265
- const cursor = this.db.common_current_data.find(
266
- {
267
- _id: { $in: lookups }
268
- },
269
- { session, projection }
270
- );
271
- for await (let doc of cursor.stream()) {
272
- current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
273
- }
274
-
275
- let persistedBatch: PersistedBatch | null = new PersistedBatch(this.db, this.group_id, transactionSize, {
276
- logger: this.logger
277
- });
278
-
250
+ using lookupSpan = this.tracer.span('storage', 'lookup');
251
+ const lookups = b.map((r) => ({
252
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
253
+ replicaId: r.beforeId
254
+ }));
255
+ let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
256
+ lookupSpan.end();
257
+
258
+ let persistedBatch: PersistedBatch | null = this.createPersistedBatch(transactionSize);
259
+
260
+ // The current code structure makes it tricky to cleanly split this span from the one
261
+ // where fluhsing. So we manually end and re-create this span whenever we flush.
262
+ let evalSpan = this.tracer.span('evaluate');
279
263
  for (let op of b) {
280
264
  if (resumeBatch) {
281
265
  resumeBatch.push(op);
282
266
  continue;
283
267
  }
284
- const currentData = current_data_lookup.get(op.internalBeforeKey) ?? null;
285
- if (currentData != null) {
268
+ const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
269
+ if (sourceRecord != null) {
286
270
  // If it will be used again later, it will be set again using nextData below
287
- current_data_lookup.delete(op.internalBeforeKey);
271
+ sourceRecordLookup.delete(op.internalBeforeKey);
288
272
  }
289
- const nextData = this.saveOperation(persistedBatch!, op, currentData, op_seq);
273
+ const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq);
290
274
  if (nextData != null) {
291
275
  // Update our current_data and size cache
292
- current_data_lookup.set(op.internalAfterKey!, nextData);
293
- sizes?.set(op.internalAfterKey!, nextData.data.length());
276
+ sourceRecordLookup.set(op.internalAfterKey!, nextData);
277
+ sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
294
278
  }
295
279
 
296
280
  if (persistedBatch!.shouldFlushTransaction()) {
281
+ evalSpan.end();
297
282
  // Transaction is getting big.
298
283
  // Flush, and resume in a new transaction.
284
+ using persistSpan = this.tracer.span('storage', 'persist_flush');
299
285
  const { flushedAny } = await persistedBatch!.flush(this.session, options);
286
+
300
287
  didFlush ||= flushedAny;
301
288
  persistedBatch = null;
302
289
  // Computing our current progress is a little tricky here, since
303
290
  // we're stopping in the middle of a batch.
304
291
  // We create a new batch, and push any remaining operations to it.
305
292
  resumeBatch = new OperationBatch();
293
+ persistSpan.end();
294
+ evalSpan = this.tracer.span('evaluate');
306
295
  }
307
296
  }
297
+ evalSpan.end();
308
298
 
309
299
  if (persistedBatch) {
310
300
  transactionSize = persistedBatch.currentSize;
301
+ using _ = this.tracer.span('storage', 'persist_flush');
311
302
  const { flushedAny } = await persistedBatch.flush(this.session, options);
312
303
  didFlush ||= flushedAny;
313
304
  }
314
305
  }
315
306
 
316
307
  if (didFlush) {
308
+ using _ = this.tracer.span('storage', 'clear_error');
317
309
  await this.clearError();
318
310
  }
319
311
 
@@ -323,7 +315,7 @@ export class MongoBucketBatch
323
315
  private saveOperation(
324
316
  batch: PersistedBatch,
325
317
  operation: RecordOperation,
326
- current_data: CurrentDataDocument | null,
318
+ sourceRecord: LoadedSourceRecord | null,
327
319
  opSeq: MongoIdSequence
328
320
  ) {
329
321
  const record = operation.record;
@@ -332,16 +324,16 @@ export class MongoBucketBatch
332
324
  let after = record.after;
333
325
  const sourceTable = record.sourceTable;
334
326
 
335
- let existing_buckets: CurrentBucket[] = [];
336
- let new_buckets: CurrentBucket[] = [];
337
- let existing_lookups: bson.Binary[] = [];
338
- let new_lookups: bson.Binary[] = [];
327
+ let existing_buckets: LoadedSourceRecord['buckets'] = [];
328
+ let new_buckets: LoadedSourceRecord['buckets'] = [];
329
+ let existing_lookups: LoadedSourceRecord['lookups'] = [];
330
+ let new_lookups: LoadedSourceRecord['lookups'] = [];
339
331
 
340
- const before_key: SourceKey = { g: this.group_id, t: mongoTableId(record.sourceTable.id), k: beforeId };
332
+ const sourceTableId = mongoTableId(record.sourceTable.id);
341
333
 
342
334
  if (this.skipExistingRows) {
343
335
  if (record.tag == SaveOperationTag.INSERT) {
344
- if (current_data != null) {
336
+ if (sourceRecord != null) {
345
337
  // Initial replication, and we already have the record.
346
338
  // This may be a different version of the record, but streaming replication
347
339
  // will take care of that.
@@ -354,7 +346,7 @@ export class MongoBucketBatch
354
346
  }
355
347
 
356
348
  if (record.tag == SaveOperationTag.UPDATE) {
357
- const result = current_data;
349
+ const result = sourceRecord;
358
350
  if (result == null) {
359
351
  // Not an error if we re-apply a transaction
360
352
  existing_buckets = [];
@@ -375,13 +367,13 @@ export class MongoBucketBatch
375
367
  } else {
376
368
  existing_buckets = result.buckets;
377
369
  existing_lookups = result.lookups;
378
- if (this.storeCurrentData) {
379
- const data = deserializeBson((result.data as mongo.Binary).buffer) as SqliteRow;
370
+ if (this.storeCurrentData && result.data != null) {
371
+ const data = deserializeBson(result.data.buffer) as SqliteRow;
380
372
  after = storage.mergeToast<SqliteValue>(after!, data);
381
373
  }
382
374
  }
383
375
  } else if (record.tag == SaveOperationTag.DELETE) {
384
- const result = current_data;
376
+ const result = sourceRecord;
385
377
  if (result == null) {
386
378
  // Not an error if we re-apply a transaction
387
379
  existing_buckets = [];
@@ -398,9 +390,9 @@ export class MongoBucketBatch
398
390
  }
399
391
  }
400
392
 
401
- let afterData: bson.Binary | undefined;
393
+ let afterData: bson.Binary | null = null;
402
394
  if (afterId != null && !this.storeCurrentData) {
403
- afterData = EMPTY_DATA;
395
+ afterData = null;
404
396
  } else if (afterId != null) {
405
397
  try {
406
398
  // This will fail immediately if the record is > 16MB.
@@ -498,13 +490,7 @@ export class MongoBucketBatch
498
490
  table: sourceTable,
499
491
  before_buckets: existing_buckets
500
492
  });
501
- new_buckets = evaluated.map((e) => {
502
- return {
503
- bucket: e.bucket,
504
- table: e.table,
505
- id: e.id
506
- };
507
- });
493
+ new_buckets = this.sourceRecordStore.mapEvaluatedBuckets(evaluated);
508
494
  }
509
495
 
510
496
  if (sourceTable.syncParameters) {
@@ -537,28 +523,29 @@ export class MongoBucketBatch
537
523
  evaluated: paramEvaluated,
538
524
  existing_lookups
539
525
  });
540
- new_lookups = paramEvaluated.map((p) => {
541
- return storage.serializeLookup(p.lookup);
542
- });
526
+ new_lookups = this.sourceRecordStore.mapParameterLookups(paramEvaluated);
543
527
  }
544
528
  }
545
529
 
546
- let result: CurrentDataDocument | null = null;
530
+ let result: LoadedSourceRecord | null = null;
547
531
 
548
532
  // 5. TOAST: Update current data and bucket list.
549
533
  if (afterId) {
550
534
  // Insert or update
551
- const after_key: SourceKey = { g: this.group_id, t: mongoTableId(sourceTable.id), k: afterId };
552
- batch.upsertCurrentData(after_key, {
535
+ batch.upsertCurrentData({
536
+ sourceTableId,
537
+ replicaId: afterId,
553
538
  data: afterData,
554
539
  buckets: new_buckets,
555
540
  lookups: new_lookups
556
541
  });
557
542
  result = {
558
- _id: after_key,
559
- data: afterData!,
543
+ sourceTableId,
544
+ replicaId: afterId,
545
+ data: afterData,
560
546
  buckets: new_buckets,
561
- lookups: new_lookups
547
+ lookups: new_lookups,
548
+ cacheKey: operation.internalAfterKey!
562
549
  };
563
550
  }
564
551
 
@@ -567,13 +554,15 @@ export class MongoBucketBatch
567
554
  // Note that this is a soft delete.
568
555
  // We don't specifically need a new or unique op_id here, but it must be greater than the
569
556
  // last checkpoint, so we use next().
570
- batch.softDeleteCurrentData(before_key, opSeq.next());
557
+ batch.softDeleteCurrentData(sourceTableId, beforeId, opSeq.next());
571
558
  }
572
559
  return result;
573
560
  }
574
561
 
575
562
  private async withTransaction(cb: () => Promise<void>) {
563
+ using lockSpan = this.tracer.span('storage', 'internal_lock');
576
564
  await replicationMutex.exclusiveLock(async () => {
565
+ lockSpan.end();
577
566
  await this.session.withTransaction(
578
567
  async () => {
579
568
  try {
@@ -584,7 +573,9 @@ export class MongoBucketBatch
584
573
  } else {
585
574
  this.logger.warn('Transaction error', e as Error);
586
575
  }
587
- await timers.setTimeout(Math.random() * 50);
576
+ const delay = Math.random() * 50;
577
+ using _ = this.tracer.span('storage', 'retry_delay');
578
+ await timers.setTimeout(delay);
588
579
  throw e;
589
580
  }
590
581
  },
@@ -848,25 +839,13 @@ export class MongoBucketBatch
848
839
  await this.db.notifyCheckpoint();
849
840
  this.persisted_op = null;
850
841
  this.last_checkpoint_lsn = lsn;
851
- if (this.db.storageConfig.softDeleteCurrentData && newLastCheckpoint != null) {
852
- await this.cleanupCurrentData(newLastCheckpoint);
842
+ if (newLastCheckpoint != null) {
843
+ await this.sourceRecordStore.postCommitCleanup(newLastCheckpoint, this.logger);
853
844
  }
854
845
  }
855
846
  return { checkpointBlocked, checkpointCreated };
856
847
  }
857
848
 
858
- private async cleanupCurrentData(lastCheckpoint: bigint) {
859
- const result = await this.db.v3_current_data.deleteMany({
860
- '_id.g': this.group_id,
861
- pending_delete: { $exists: true, $lte: lastCheckpoint }
862
- });
863
- if (result.deletedCount > 0) {
864
- this.logger.info(
865
- `Cleaned up ${result.deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`
866
- );
867
- }
868
- }
869
-
870
849
  /**
871
850
  * Switch from processing -> active if relevant.
872
851
  *
@@ -915,7 +894,7 @@ export class MongoBucketBatch
915
894
  }
916
895
  });
917
896
  if (activated) {
918
- this.logger.info(`Activated new sync rules at ${lsn}`);
897
+ this.logger.info(`Activated new replication stream at ${lsn}`);
919
898
  await this.db.notifyCheckpoint();
920
899
  this.needsActivation = false;
921
900
  }
@@ -988,9 +967,11 @@ export class MongoBucketBatch
988
967
 
989
968
  await this.withTransaction(async () => {
990
969
  for (let table of sourceTables) {
991
- await this.db.source_tables.deleteOne({ _id: mongoTableId(table.id) });
970
+ await this.db.commonSourceTables(this.group_id).deleteOne({ _id: mongoTableId(table.id) });
992
971
  }
993
972
  });
973
+
974
+ await this.cleanupDroppedSourceTables(sourceTables);
994
975
  return result;
995
976
  }
996
977
 
@@ -1022,24 +1003,10 @@ export class MongoBucketBatch
1022
1003
  let lastBatchCount = BATCH_LIMIT;
1023
1004
  while (lastBatchCount == BATCH_LIMIT) {
1024
1005
  await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
1025
- const current_data_filter: mongo.Filter<CurrentDataDocument> = {
1026
- _id: idPrefixFilter<SourceKey>({ g: this.group_id, t: mongoTableId(sourceTable.id) }, ['k']),
1027
- // Skip soft-deleted data
1028
- // Works for both v1 and v3 current_data schemas
1029
- pending_delete: { $exists: false }
1030
- };
1031
-
1032
- const cursor = this.db.common_current_data.find(current_data_filter, {
1033
- projection: {
1034
- _id: 1,
1035
- buckets: 1,
1036
- lookups: 1
1037
- },
1038
- limit: BATCH_LIMIT,
1039
- session: session
1040
- });
1041
- const batch = await cursor.toArray();
1042
- const persistedBatch = new PersistedBatch(this.db, this.group_id, 0, { logger: this.logger });
1006
+ using evalSpan = this.tracer.span('evaluate');
1007
+ const sourceTableId = mongoTableId(sourceTable.id);
1008
+ const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
1009
+ const persistedBatch = this.createPersistedBatch(0);
1043
1010
 
1044
1011
  for (let value of batch) {
1045
1012
  persistedBatch.saveBucketData({
@@ -1047,19 +1014,22 @@ export class MongoBucketBatch
1047
1014
  before_buckets: value.buckets,
1048
1015
  evaluated: [],
1049
1016
  table: sourceTable,
1050
- sourceKey: value._id.k
1017
+ sourceKey: value.replicaId
1051
1018
  });
1052
1019
  persistedBatch.saveParameterData({
1053
1020
  op_seq: opSeq,
1054
1021
  existing_lookups: value.lookups,
1055
1022
  evaluated: [],
1056
1023
  sourceTable: sourceTable,
1057
- sourceKey: value._id.k
1024
+ sourceKey: value.replicaId
1058
1025
  });
1059
1026
 
1060
1027
  // Since this is not from streaming replication, we can do a hard delete
1061
- persistedBatch.hardDeleteCurrentData(value._id);
1028
+ persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
1062
1029
  }
1030
+ evalSpan.end();
1031
+
1032
+ using _ = this.tracer.span('storage', 'persist_flush');
1063
1033
  await persistedBatch.flush(session);
1064
1034
  lastBatchCount = batch.length;
1065
1035
 
@@ -1083,7 +1053,7 @@ export class MongoBucketBatch
1083
1053
  copy.snapshotStatus = snapshotStatus;
1084
1054
 
1085
1055
  await this.withTransaction(async () => {
1086
- await this.db.source_tables.updateOne(
1056
+ await this.db.commonSourceTables(this.group_id).updateOne(
1087
1057
  { _id: mongoTableId(table.id) },
1088
1058
  {
1089
1059
  $set: {
@@ -1138,7 +1108,7 @@ export class MongoBucketBatch
1138
1108
  const ids = tables.map((table) => mongoTableId(table.id));
1139
1109
 
1140
1110
  await this.withTransaction(async () => {
1141
- await this.db.source_tables.updateMany(
1111
+ await this.db.commonSourceTables(this.group_id).updateMany(
1142
1112
  { _id: { $in: ids } },
1143
1113
  {
1144
1114
  $set: {
@@ -1204,7 +1174,3 @@ export class MongoBucketBatch
1204
1174
  );
1205
1175
  }
1206
1176
  }
1207
-
1208
- export function currentBucketKey(b: CurrentBucket) {
1209
- return `${b.bucket}/${b.table}/${b.id}`;
1210
- }
@@ -0,0 +1,11 @@
1
+ import * as bson from 'bson';
2
+ import { SourceRecordBucketState } from './common/SourceRecordStore.js';
3
+
4
+ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
5
+
6
+ export const EMPTY_DATA = new bson.Binary(bson.serialize({}));
7
+
8
+ export function currentBucketKey(bucket: SourceRecordBucketState) {
9
+ const prefix = bucket.definitionId == null ? '' : `${bucket.definitionId}:`;
10
+ return `${prefix}${bucket.bucket}/${bucket.table}/${bucket.id}`;
11
+ }