@powersync/service-module-mongodb-storage 0.18.3 → 0.20.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.
- package/CHANGELOG.md +53 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +13 -12
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +4 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +58 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +19 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +35 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +108 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +43 -2
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +9 -1
- package/dist/storage/implementation/v3/source-table-utils.js +20 -21
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +23 -22
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +20 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +76 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +53 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +40 -23
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +122 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +45 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +43 -35
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +124 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
2
2
|
import { ColumnDescriptor, InternalOpId, SourceTable, storage } from '@powersync/service-core';
|
|
3
|
+
import { HydratedSyncConfig } from '@powersync/service-sync-rules';
|
|
3
4
|
import * as bson from 'bson';
|
|
4
5
|
import { mongoTableId } from '../../../utils/util.js';
|
|
5
6
|
import { calculateCheckpointState } from '../CheckpointState.js';
|
|
@@ -9,7 +10,7 @@ import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
|
9
10
|
import { PersistedBatchV1 } from './PersistedBatchV1.js';
|
|
10
11
|
import { SourceRecordStoreV1 } from './SourceRecordStoreV1.js';
|
|
11
12
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
12
|
-
import { SyncRuleDocumentV1 } from './models.js';
|
|
13
|
+
import { SourceTableDocumentV1, SyncRuleDocumentV1 } from './models.js';
|
|
13
14
|
|
|
14
15
|
export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
15
16
|
declare readonly db: VersionedPowerSyncMongoV1;
|
|
@@ -51,6 +52,7 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
51
52
|
|
|
52
53
|
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
53
54
|
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
55
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
54
56
|
const { connection_id, source } = options;
|
|
55
57
|
const { schema, name, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
56
58
|
|
|
@@ -63,20 +65,38 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
63
65
|
let result: storage.ResolveTablesResult | null = null;
|
|
64
66
|
await this.db.client.withSession(async (session) => {
|
|
65
67
|
const col = this.db.sourceTablesV1(this.replicationStreamId);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
schema_name: schema,
|
|
70
|
-
table_name: name,
|
|
71
|
-
replica_id_columns2: normalizedReplicaIdColumns
|
|
72
|
-
};
|
|
68
|
+
|
|
69
|
+
// Find records that overlap by name or relation id.
|
|
70
|
+
const orClauses: Record<string, unknown>[] = [{ schema_name: schema, table_name: name }];
|
|
73
71
|
if (objectId != null) {
|
|
74
|
-
|
|
72
|
+
orClauses.push({ relation_id: objectId });
|
|
73
|
+
}
|
|
74
|
+
const candidateDocs = await col
|
|
75
|
+
.find({ group_id: this.replicationStreamId, connection_id, $or: orClauses }, { session })
|
|
76
|
+
.toArray();
|
|
77
|
+
|
|
78
|
+
const candidateTables = candidateDocs.map((doc) =>
|
|
79
|
+
this.hydrateSourceTable(doc, connectionTag, syncRules, sendsCompleteRows)
|
|
80
|
+
);
|
|
81
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
82
|
+
const resolution = await reconcile({ source, candidates });
|
|
83
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
84
|
+
|
|
85
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
86
|
+
await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
90
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
91
|
+
|
|
92
|
+
// V1 keeps one record, preferring one that has already been snapshotted.
|
|
93
|
+
const reuse = compatibleTables.find((table) => table.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
94
|
+
|
|
95
|
+
let sourceTable: storage.SourceTable;
|
|
96
|
+
if (reuse != null) {
|
|
97
|
+
sourceTable = reuse;
|
|
98
|
+
} else {
|
|
99
|
+
const doc: SourceTableDocumentV1 = {
|
|
80
100
|
_id: options.idGenerator ? (options.idGenerator() as bson.ObjectId) : new bson.ObjectId(),
|
|
81
101
|
group_id: this.replicationStreamId,
|
|
82
102
|
connection_id,
|
|
@@ -86,69 +106,18 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
86
106
|
replica_id_columns: null,
|
|
87
107
|
replica_id_columns2: normalizedReplicaIdColumns,
|
|
88
108
|
snapshot_done: false,
|
|
89
|
-
snapshot_status: undefined
|
|
109
|
+
snapshot_status: undefined,
|
|
110
|
+
source_metadata: resolution.newTableValues.sourceMetadata
|
|
90
111
|
};
|
|
91
112
|
await col.insertOne(doc, { session });
|
|
113
|
+
sourceTable = this.hydrateSourceTable(doc, connectionTag, syncRules, sendsCompleteRows);
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
100
|
-
...syncRules.getMatchingSources(source)
|
|
101
|
-
});
|
|
102
|
-
sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
|
|
103
|
-
sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
|
|
104
|
-
sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
|
|
105
|
-
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
106
|
-
sourceTable.snapshotStatus =
|
|
107
|
-
doc.snapshot_status == null
|
|
108
|
-
? undefined
|
|
109
|
-
: {
|
|
110
|
-
lastKey: doc.snapshot_status.last_key?.buffer ?? null,
|
|
111
|
-
totalEstimatedCount: doc.snapshot_status.total_estimated_count,
|
|
112
|
-
replicatedCount: doc.snapshot_status.replicated_count
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const truncateFilter = [{ schema_name: schema, table_name: name }] as any[];
|
|
116
|
-
if (objectId != null) {
|
|
117
|
-
truncateFilter.push({ relation_id: objectId });
|
|
118
|
-
}
|
|
119
|
-
const truncate = await col
|
|
120
|
-
.find(
|
|
121
|
-
{
|
|
122
|
-
group_id: this.replicationStreamId,
|
|
123
|
-
connection_id,
|
|
124
|
-
_id: { $ne: doc._id },
|
|
125
|
-
$or: truncateFilter
|
|
126
|
-
},
|
|
127
|
-
{ session }
|
|
128
|
-
)
|
|
129
|
-
.toArray();
|
|
130
|
-
const dropTables = truncate.map((dropDoc) => {
|
|
131
|
-
const ref = {
|
|
132
|
-
connectionTag,
|
|
133
|
-
schema: dropDoc.schema_name,
|
|
134
|
-
name: dropDoc.table_name
|
|
135
|
-
};
|
|
136
|
-
const table = new storage.SourceTable({
|
|
137
|
-
id: dropDoc._id,
|
|
138
|
-
ref,
|
|
139
|
-
objectId: dropDoc.relation_id,
|
|
140
|
-
replicaIdColumns:
|
|
141
|
-
dropDoc.replica_id_columns2?.map(
|
|
142
|
-
(c) => ({ name: c.name, typeId: c.type_oid, type: c.type }) satisfies ColumnDescriptor
|
|
143
|
-
) ?? [],
|
|
144
|
-
snapshotComplete: dropDoc.snapshot_done ?? true,
|
|
145
|
-
...syncRules.getMatchingSources(ref)
|
|
146
|
-
});
|
|
147
|
-
table.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
148
|
-
table.syncData = table.bucketDataSources.length > 0;
|
|
149
|
-
table.syncParameters = table.parameterLookupSources.length > 0;
|
|
150
|
-
return table;
|
|
151
|
-
});
|
|
116
|
+
const dropTables = [
|
|
117
|
+
...materializedResolution.incompatibleTables,
|
|
118
|
+
// V1 only keeps one SourceTable per physical table.
|
|
119
|
+
...compatibleTables.filter((table) => !storage.sourceTableIdEquals(table.id, sourceTable.id))
|
|
120
|
+
];
|
|
152
121
|
|
|
153
122
|
result = { tables: [sourceTable], dropTables };
|
|
154
123
|
});
|
|
@@ -156,38 +125,33 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
156
125
|
return result!;
|
|
157
126
|
}
|
|
158
127
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const ref = {
|
|
172
|
-
connectionTag: table.ref.connectionTag,
|
|
173
|
-
schema: doc.schema_name,
|
|
174
|
-
name: doc.table_name
|
|
175
|
-
};
|
|
176
|
-
const sourceTable = new storage.SourceTable({
|
|
128
|
+
/**
|
|
129
|
+
* Build a SourceTable from its persisted v1 record.
|
|
130
|
+
*/
|
|
131
|
+
private hydrateSourceTable(
|
|
132
|
+
doc: SourceTableDocumentV1,
|
|
133
|
+
connectionTag: string,
|
|
134
|
+
syncRules: HydratedSyncConfig,
|
|
135
|
+
sendsCompleteRows: boolean | undefined
|
|
136
|
+
): storage.SourceTable {
|
|
137
|
+
const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
|
|
138
|
+
const table = new storage.SourceTable({
|
|
177
139
|
id: doc._id,
|
|
178
140
|
ref,
|
|
179
141
|
objectId: doc.relation_id,
|
|
180
142
|
replicaIdColumns:
|
|
181
143
|
doc.replica_id_columns2?.map(
|
|
182
|
-
(
|
|
144
|
+
(column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
|
|
183
145
|
) ?? [],
|
|
184
146
|
snapshotComplete: doc.snapshot_done ?? true,
|
|
185
|
-
|
|
147
|
+
sourceMetadata: doc.source_metadata ?? null,
|
|
148
|
+
...syncRules.getMatchingSources(ref)
|
|
186
149
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
150
|
+
table.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
151
|
+
table.syncData = table.bucketDataSources.length > 0;
|
|
152
|
+
table.syncParameters = table.parameterLookupSources.length > 0;
|
|
153
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
154
|
+
table.snapshotStatus =
|
|
191
155
|
doc.snapshot_status == null
|
|
192
156
|
? undefined
|
|
193
157
|
: {
|
|
@@ -195,7 +159,23 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
195
159
|
totalEstimatedCount: doc.snapshot_status.total_estimated_count,
|
|
196
160
|
replicatedCount: doc.snapshot_status.replicated_count
|
|
197
161
|
};
|
|
198
|
-
return
|
|
162
|
+
return table;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null> {
|
|
166
|
+
const doc = await this.db.sourceTablesV1(this.replicationStreamId).findOne(
|
|
167
|
+
{
|
|
168
|
+
group_id: this.replicationStreamId,
|
|
169
|
+
_id: mongoTableId(table.id)
|
|
170
|
+
},
|
|
171
|
+
{ session: this.session }
|
|
172
|
+
);
|
|
173
|
+
if (doc == null) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Use the default storeCurrentData value when reading outside resolution.
|
|
178
|
+
return this.hydrateSourceTable(doc, table.ref.connectionTag, this.sync_rules, undefined);
|
|
199
179
|
}
|
|
200
180
|
|
|
201
181
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { storage } from '@powersync/service-core';
|
|
3
|
+
import { addChecksums, storage, utils } from '@powersync/service-core';
|
|
4
4
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
|
-
import {
|
|
5
|
+
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
6
|
import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
|
|
7
|
-
import { DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
8
|
-
import {
|
|
7
|
+
import { CurrentBucketState, DirtyBucket, MongoCompactor } from '../MongoCompactor.js';
|
|
8
|
+
import { cacheKey } from '../OperationBatch.js';
|
|
9
|
+
import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
|
|
9
10
|
import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
|
|
10
11
|
import { SingleBucketStoreV1 } from './SingleBucketStoreV1.js';
|
|
11
12
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
12
13
|
|
|
14
|
+
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
15
|
+
|
|
13
16
|
export class MongoCompactorV1 extends MongoCompactor {
|
|
14
17
|
// Override types to the more specific ones
|
|
15
18
|
declare protected readonly db: VersionedPowerSyncMongoV1;
|
|
16
19
|
declare protected readonly storage: MongoSyncBucketStorageV1;
|
|
17
20
|
|
|
21
|
+
private updates: mongo.AnyBulkWriteOperation<BucketDataDocumentV1>[] = [];
|
|
22
|
+
|
|
18
23
|
public async *dirtyBucketBatches(options: {
|
|
19
24
|
minBucketChanges: number;
|
|
20
25
|
minChangeRatio: number;
|
|
@@ -81,14 +86,293 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
84
|
-
|
|
85
|
-
bucket: string,
|
|
86
|
-
_definitionId: BucketDefinitionId | null
|
|
87
|
-
): Promise<SingleBucketStore | null> {
|
|
89
|
+
private getBucketDataContext(bucket: string): SingleBucketStoreV1 {
|
|
88
90
|
return new SingleBucketStoreV1(this.db, {
|
|
89
91
|
replicationStreamId: this.group_id,
|
|
90
92
|
definitionId: LEGACY_BUCKET_DATA_DEFINITION_ID,
|
|
91
93
|
bucket
|
|
92
94
|
});
|
|
93
95
|
}
|
|
96
|
+
|
|
97
|
+
protected async compactSingleBucket(bucket: string) {
|
|
98
|
+
// Do not carry queued writes from a failed attempt into the rescan
|
|
99
|
+
this.updates = [];
|
|
100
|
+
|
|
101
|
+
const idLimitBytes = this.idLimitBytes;
|
|
102
|
+
const bucketContext = this.getBucketDataContext(bucket);
|
|
103
|
+
const currentState: CurrentBucketState = {
|
|
104
|
+
bucket,
|
|
105
|
+
definitionId: bucketContext.key.definitionId,
|
|
106
|
+
seen: new Map(),
|
|
107
|
+
trackingSize: 0,
|
|
108
|
+
lastNotPut: null,
|
|
109
|
+
opsSincePut: 0,
|
|
110
|
+
checksum: 0,
|
|
111
|
+
opCount: 0,
|
|
112
|
+
opBytes: 0
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Constant lower bound.
|
|
116
|
+
const lowerBound = bucketContext.minId;
|
|
117
|
+
// Upper bound is adjusted for each batch.
|
|
118
|
+
let upperBound = bucketContext.maxId;
|
|
119
|
+
|
|
120
|
+
// Paginate through bucket data in batches to avoid cursor timeouts.
|
|
121
|
+
while (true) {
|
|
122
|
+
this.signal?.throwIfAborted();
|
|
123
|
+
|
|
124
|
+
// Query one batch at a time, to avoid cursor timeouts.
|
|
125
|
+
const pipeline = [
|
|
126
|
+
{
|
|
127
|
+
$match: {
|
|
128
|
+
_id: {
|
|
129
|
+
$gte: lowerBound,
|
|
130
|
+
$lt: upperBound
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ $sort: { _id: -1 } },
|
|
135
|
+
{ $limit: this.moveBatchQueryLimit },
|
|
136
|
+
{
|
|
137
|
+
$project: {
|
|
138
|
+
_id: 1,
|
|
139
|
+
op: 1,
|
|
140
|
+
table: 1,
|
|
141
|
+
row_id: 1,
|
|
142
|
+
source_table: 1,
|
|
143
|
+
source_key: 1,
|
|
144
|
+
checksum: 1,
|
|
145
|
+
size: { $bsonSize: '$$ROOT' }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const cursor = bucketContext.collection.aggregate<BucketDataDocumentV1 & { size: number | bigint }>(pipeline, {
|
|
151
|
+
// batchSize is 1 more than limit to auto-close the cursor.
|
|
152
|
+
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
153
|
+
batchSize: this.moveBatchQueryLimit + 1
|
|
154
|
+
});
|
|
155
|
+
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
156
|
+
// Instead, we load up to the limit.
|
|
157
|
+
const rawBatch = await cursor.toArray();
|
|
158
|
+
const batch = rawBatch.map((document) => {
|
|
159
|
+
const { size, ...rest } = document;
|
|
160
|
+
return {
|
|
161
|
+
doc: bucketContext.fromPersistedDocument(rest),
|
|
162
|
+
size
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (batch.length == 0) {
|
|
167
|
+
// We've reached the end.
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Reuse the exact collection _id value from Mongo for the next bound.
|
|
172
|
+
upperBound = rawBatch[rawBatch.length - 1]._id;
|
|
173
|
+
|
|
174
|
+
for (const { doc, size } of batch) {
|
|
175
|
+
if (doc.o > this.maxOpId) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
currentState.checksum = addChecksums(currentState.checksum, Number(doc.checksum));
|
|
180
|
+
currentState.opCount += 1;
|
|
181
|
+
|
|
182
|
+
let isPersistentPut = doc.op == 'PUT';
|
|
183
|
+
|
|
184
|
+
currentState.opBytes += Number(size);
|
|
185
|
+
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
186
|
+
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
|
|
187
|
+
const targetOp = currentState.seen.get(key);
|
|
188
|
+
if (targetOp) {
|
|
189
|
+
// Will convert to MOVE, so don't count as PUT.
|
|
190
|
+
isPersistentPut = false;
|
|
191
|
+
|
|
192
|
+
this.updates.push({
|
|
193
|
+
updateOne: {
|
|
194
|
+
filter: { _id: bucketContext.docId(doc.o) },
|
|
195
|
+
update: {
|
|
196
|
+
$set: {
|
|
197
|
+
op: 'MOVE',
|
|
198
|
+
target_op: targetOp
|
|
199
|
+
},
|
|
200
|
+
$unset: {
|
|
201
|
+
source_table: 1,
|
|
202
|
+
source_key: 1,
|
|
203
|
+
table: 1,
|
|
204
|
+
row_id: 1,
|
|
205
|
+
data: 1
|
|
206
|
+
}
|
|
207
|
+
} satisfies mongo.UpdateFilter<BucketDataDocumentV1>
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// TODO: better estimate for this.
|
|
212
|
+
currentState.opBytes += 200 - Number(size);
|
|
213
|
+
} else if (currentState.trackingSize < idLimitBytes) {
|
|
214
|
+
// flatstr reduces the memory usage by flattening the string.
|
|
215
|
+
currentState.seen.set(utils.flatstr(key), doc.o);
|
|
216
|
+
// length + 16 for the string
|
|
217
|
+
// 24 for the bigint
|
|
218
|
+
// 50 for map overhead
|
|
219
|
+
// 50 for additional overhead
|
|
220
|
+
currentState.trackingSize += key.length + 140;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (isPersistentPut) {
|
|
225
|
+
currentState.lastNotPut = null;
|
|
226
|
+
currentState.opsSincePut = 0;
|
|
227
|
+
} else if (doc.op != 'CLEAR') {
|
|
228
|
+
if (currentState.lastNotPut == null) {
|
|
229
|
+
currentState.lastNotPut = doc.o;
|
|
230
|
+
}
|
|
231
|
+
currentState.opsSincePut += 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
|
|
235
|
+
await this.flush(bucketContext);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Free memory before clearing the bucket.
|
|
243
|
+
currentState.seen.clear();
|
|
244
|
+
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
245
|
+
this.logger.info(
|
|
246
|
+
`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
247
|
+
);
|
|
248
|
+
// Need flush() before clear().
|
|
249
|
+
await this.flush(bucketContext);
|
|
250
|
+
await this.clearBucket(currentState, bucketContext);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Do this after clearBucket so we have accurate counts.
|
|
254
|
+
this.updateBucketChecksums(currentState, this.maxOpId);
|
|
255
|
+
// Need another flush after updateBucketChecksums().
|
|
256
|
+
await this.flush(bucketContext);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async flush(bucketContext: SingleBucketStoreV1) {
|
|
260
|
+
if (this.updates.length > 0) {
|
|
261
|
+
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
262
|
+
await bucketContext.collection.bulkWrite(this.updates, {
|
|
263
|
+
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
264
|
+
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
265
|
+
ordered: false
|
|
266
|
+
});
|
|
267
|
+
this.updates = [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
await this.flushBucketStateUpdates();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Perform a CLEAR compact for a bucket.
|
|
275
|
+
*
|
|
276
|
+
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
277
|
+
*/
|
|
278
|
+
private async clearBucket(currentState: CurrentBucketState, bucketContext: SingleBucketStoreV1) {
|
|
279
|
+
const clearOp = currentState.lastNotPut!;
|
|
280
|
+
|
|
281
|
+
const opFilter = {
|
|
282
|
+
_id: {
|
|
283
|
+
$gte: bucketContext.minId,
|
|
284
|
+
$lte: bucketContext.docId(clearOp)
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const session = this.db.client.startSession();
|
|
289
|
+
try {
|
|
290
|
+
let done = false;
|
|
291
|
+
while (!done) {
|
|
292
|
+
this.signal?.throwIfAborted();
|
|
293
|
+
let opCountDiff = 0;
|
|
294
|
+
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
295
|
+
// The state after each batch is fully consistent.
|
|
296
|
+
// We need a transaction per batch to make sure checksums stay consistent.
|
|
297
|
+
await session.withTransaction(
|
|
298
|
+
async () => {
|
|
299
|
+
const query = bucketContext.collection.find<Pick<BucketDataDocumentV1, '_id' | CompactClearProperties>>(
|
|
300
|
+
opFilter,
|
|
301
|
+
{
|
|
302
|
+
session,
|
|
303
|
+
sort: { _id: 1 },
|
|
304
|
+
projection: {
|
|
305
|
+
_id: 1,
|
|
306
|
+
op: 1,
|
|
307
|
+
checksum: 1,
|
|
308
|
+
target_op: 1
|
|
309
|
+
},
|
|
310
|
+
limit: this.clearBatchLimit
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
let checksum = 0;
|
|
314
|
+
let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
|
|
315
|
+
let targetOp: bigint | null = null;
|
|
316
|
+
let gotAnOp = false;
|
|
317
|
+
let numberOfOpsToClear = 0;
|
|
318
|
+
for await (const rawOp of query.stream()) {
|
|
319
|
+
const op = bucketContext.fromPartialPersistedDocument(rawOp);
|
|
320
|
+
|
|
321
|
+
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
322
|
+
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
323
|
+
lastOp = op;
|
|
324
|
+
numberOfOpsToClear += 1;
|
|
325
|
+
if (op.op != 'CLEAR') {
|
|
326
|
+
gotAnOp = true;
|
|
327
|
+
}
|
|
328
|
+
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
329
|
+
targetOp = op.target_op;
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!gotAnOp) {
|
|
336
|
+
done = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
341
|
+
await bucketContext.collection.deleteMany(
|
|
342
|
+
{
|
|
343
|
+
_id: {
|
|
344
|
+
$gte: bucketContext.minId,
|
|
345
|
+
$lte: bucketContext.docId(lastOp!.o)
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
{ session }
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const op = bucketContext.toPersistedDocument({
|
|
352
|
+
o: lastOp!.o,
|
|
353
|
+
op: 'CLEAR',
|
|
354
|
+
checksum: BigInt(checksum),
|
|
355
|
+
data: null,
|
|
356
|
+
target_op: targetOp
|
|
357
|
+
});
|
|
358
|
+
await bucketContext.collection.insertOne(op, { session });
|
|
359
|
+
|
|
360
|
+
opCountDiff = -numberOfOpsToClear + 1;
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
writeConcern: { w: 'majority' },
|
|
364
|
+
readConcern: { level: 'snapshot' }
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
368
|
+
currentState.opCount += opCountDiff;
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
await session.endSession();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private formatBucketDataKey(doc: Pick<BucketDataDoc, 'bucketKey' | 'o'>) {
|
|
376
|
+
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
377
|
+
}
|
|
94
378
|
}
|