@powersync/service-module-mongodb 0.0.0-dev-20260909133214 → 0.0.0-dev-20260929095716

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 (88) hide show
  1. package/CHANGELOG.md +38 -6
  2. package/ci/test-connection.yaml +12 -0
  3. package/dist/api/MongoRouteAPIAdapter.d.ts +2 -2
  4. package/dist/api/MongoRouteAPIAdapter.js +73 -143
  5. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  6. package/dist/api/infer-collection-schema.d.ts +12 -0
  7. package/dist/api/infer-collection-schema.js +142 -0
  8. package/dist/api/infer-collection-schema.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/module/MongoModule.d.ts +1 -2
  13. package/dist/module/MongoModule.js +0 -1
  14. package/dist/module/MongoModule.js.map +1 -1
  15. package/dist/replication/ChangeStream.d.ts +10 -2
  16. package/dist/replication/ChangeStream.js +228 -317
  17. package/dist/replication/ChangeStream.js.map +1 -1
  18. package/dist/replication/ChangeStreamReplicationJob.d.ts +4 -1
  19. package/dist/replication/ChangeStreamReplicationJob.js +4 -1
  20. package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
  21. package/dist/replication/ChangeStreamReplicator.d.ts +3 -0
  22. package/dist/replication/ChangeStreamReplicator.js +3 -0
  23. package/dist/replication/ChangeStreamReplicator.js.map +1 -1
  24. package/dist/replication/MongoRelation.js +5 -2
  25. package/dist/replication/MongoRelation.js.map +1 -1
  26. package/dist/replication/MongoReplicationQueryProvider.d.ts +87 -0
  27. package/dist/replication/MongoReplicationQueryProvider.js +23 -0
  28. package/dist/replication/MongoReplicationQueryProvider.js.map +1 -0
  29. package/dist/replication/MongoReplicationStream.d.ts +85 -0
  30. package/dist/replication/MongoReplicationStream.js +101 -0
  31. package/dist/replication/MongoReplicationStream.js.map +1 -0
  32. package/dist/replication/MongoSnapshotQuery.d.ts +2 -0
  33. package/dist/replication/MongoSnapshotQuery.js +12 -2
  34. package/dist/replication/MongoSnapshotQuery.js.map +1 -1
  35. package/dist/replication/MongoSnapshotter.d.ts +7 -2
  36. package/dist/replication/MongoSnapshotter.js +58 -85
  37. package/dist/replication/MongoSnapshotter.js.map +1 -1
  38. package/dist/replication/RawChangeStream.d.ts +1 -1
  39. package/dist/replication/RawChangeStream.js +1 -1
  40. package/dist/replication/RawChangeStream.js.map +1 -1
  41. package/dist/replication/SourceRowConverter.d.ts +1 -0
  42. package/dist/replication/SourceRowConverter.js +4 -2
  43. package/dist/replication/SourceRowConverter.js.map +1 -1
  44. package/dist/replication/bufferToSqlite.d.ts +3 -1
  45. package/dist/replication/bufferToSqlite.js +35 -23
  46. package/dist/replication/bufferToSqlite.js.map +1 -1
  47. package/dist/replication/replication-index.d.ts +6 -0
  48. package/dist/replication/replication-index.js +6 -0
  49. package/dist/replication/replication-index.js.map +1 -1
  50. package/dist/test-utils/ChangeStreamTestContext.d.ts +131 -0
  51. package/dist/test-utils/ChangeStreamTestContext.js +391 -0
  52. package/dist/test-utils/ChangeStreamTestContext.js.map +1 -0
  53. package/dist/test-utils/test-utils-index.d.ts +1 -0
  54. package/dist/test-utils/test-utils-index.js +2 -0
  55. package/dist/test-utils/test-utils-index.js.map +1 -0
  56. package/package.json +8 -8
  57. package/src/api/MongoRouteAPIAdapter.ts +79 -127
  58. package/src/api/infer-collection-schema.ts +153 -0
  59. package/src/index.ts +1 -0
  60. package/src/module/MongoModule.ts +1 -3
  61. package/src/replication/ChangeStream.ts +253 -359
  62. package/src/replication/ChangeStreamReplicationJob.ts +7 -2
  63. package/src/replication/ChangeStreamReplicator.ts +5 -0
  64. package/src/replication/MongoRelation.ts +8 -1
  65. package/src/replication/MongoReplicationQueryProvider.ts +108 -0
  66. package/src/replication/MongoReplicationStream.ts +201 -0
  67. package/src/replication/MongoSnapshotQuery.ts +19 -3
  68. package/src/replication/MongoSnapshotter.ts +70 -92
  69. package/src/replication/RawChangeStream.ts +2 -2
  70. package/src/replication/SourceRowConverter.ts +4 -2
  71. package/src/replication/bufferToSqlite.ts +43 -23
  72. package/src/replication/replication-index.ts +6 -0
  73. package/src/test-utils/ChangeStreamTestContext.ts +520 -0
  74. package/src/test-utils/test-utils-index.ts +1 -0
  75. package/test/src/buffer_to_sqlite.test.ts +9 -0
  76. package/test/src/change_stream.test.ts +3 -3
  77. package/test/src/change_stream_test_setup.ts +38 -0
  78. package/test/src/chunked_snapshot.test.ts +3 -3
  79. package/test/src/documentdb_mode.test.ts +3 -3
  80. package/test/src/replication_stream.test.ts +311 -0
  81. package/test/src/resume.test.ts +10 -4
  82. package/test/src/resuming_snapshots.test.ts +3 -3
  83. package/test/src/schema.test.ts +223 -0
  84. package/test/src/slow_tests.test.ts +3 -3
  85. package/test/src/snapshot_query_provider.test.ts +95 -0
  86. package/test/src/stream_progress.test.ts +312 -0
  87. package/tsconfig.tsbuildinfo +1 -1
  88. package/test/src/change_stream_utils.ts +0 -375
@@ -3,18 +3,22 @@ import { replication } from '@powersync/service-core';
3
3
 
4
4
  import { ChangeStream, ChangeStreamInvalidatedError } from './ChangeStream.js';
5
5
  import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
6
+ import { MongoReplicationQueryProviderFactory } from './MongoReplicationQueryProvider.js';
6
7
 
7
8
  export interface ChangeStreamReplicationJobOptions extends replication.AbstractReplicationJobOptions {
8
9
  connectionFactory: ConnectionManagerFactory;
10
+ createReplicationQueryProvider?: MongoReplicationQueryProviderFactory;
9
11
  }
10
12
 
11
13
  export class ChangeStreamReplicationJob extends replication.AbstractReplicationJob {
12
- private connectionFactory: ConnectionManagerFactory;
14
+ private readonly connectionFactory: ConnectionManagerFactory;
15
+ private readonly createReplicationQueryProvider: MongoReplicationQueryProviderFactory | undefined;
13
16
  private lastStream: ChangeStream | null = null;
14
17
 
15
18
  constructor(options: ChangeStreamReplicationJobOptions) {
16
19
  super(options);
17
20
  this.connectionFactory = options.connectionFactory;
21
+ this.createReplicationQueryProvider = options.createReplicationQueryProvider;
18
22
  // We use a custom formatter to process the prefix
19
23
  this.logger = options.storage.logger;
20
24
  }
@@ -72,7 +76,8 @@ export class ChangeStreamReplicationJob extends replication.AbstractReplicationJ
72
76
  metrics: this.options.metrics,
73
77
  connections: connectionManager,
74
78
  keepaliveIntervalMs: connectionManager.options.heartbeat_interval_seconds * 1_000,
75
- logger: this.logger
79
+ logger: this.logger,
80
+ createReplicationQueryProvider: this.createReplicationQueryProvider
76
81
  });
77
82
  this.lastStream = stream;
78
83
  await stream.replicate();
@@ -2,17 +2,21 @@ import { replication, storage } from '@powersync/service-core';
2
2
  import { MongoModule } from '../module/MongoModule.js';
3
3
  import { ChangeStreamReplicationJob } from './ChangeStreamReplicationJob.js';
4
4
  import { ConnectionManagerFactory } from './ConnectionManagerFactory.js';
5
+ import { MongoReplicationQueryProviderFactory } from './MongoReplicationQueryProvider.js';
5
6
 
6
7
  export interface ChangeStreamReplicatorOptions extends replication.AbstractReplicatorOptions {
7
8
  connectionFactory: ConnectionManagerFactory;
9
+ createReplicationQueryProvider?: MongoReplicationQueryProviderFactory;
8
10
  }
9
11
 
10
12
  export class ChangeStreamReplicator extends replication.AbstractReplicator<ChangeStreamReplicationJob> {
11
13
  private readonly connectionFactory: ConnectionManagerFactory;
14
+ private readonly createReplicationQueryProvider: MongoReplicationQueryProviderFactory | undefined;
12
15
 
13
16
  constructor(options: ChangeStreamReplicatorOptions) {
14
17
  super(options);
15
18
  this.connectionFactory = options.connectionFactory;
19
+ this.createReplicationQueryProvider = options.createReplicationQueryProvider;
16
20
  }
17
21
 
18
22
  createJob(options: replication.CreateJobOptions): ChangeStreamReplicationJob {
@@ -21,6 +25,7 @@ export class ChangeStreamReplicator extends replication.AbstractReplicator<Chang
21
25
  storage: options.storage,
22
26
  metrics: this.metrics,
23
27
  connectionFactory: this.connectionFactory,
28
+ createReplicationQueryProvider: this.createReplicationQueryProvider,
24
29
  lock: options.lock,
25
30
  rateLimiter: this.rateLimiter
26
31
  });
@@ -3,11 +3,14 @@ import { storage } from '@powersync/service-core';
3
3
  import { JsonContainer } from '@powersync/service-jsonbig';
4
4
  import {
5
5
  CompatibilityContext,
6
+ CompatibilityOption,
6
7
  CustomArray,
7
8
  CustomObject,
8
9
  CustomSqliteValue,
9
10
  DateTimeSourceOptions,
10
11
  DateTimeValue,
12
+ SQLITE_FALSE,
13
+ SQLITE_TRUE,
11
14
  SqliteInputRow,
12
15
  SqliteInputValue,
13
16
  TimeValuePrecision
@@ -127,7 +130,11 @@ function filterJsonData(data: any, context: CompatibilityContext, depth = 0): an
127
130
  return data;
128
131
  }
129
132
  } else if (typeof data == 'boolean') {
130
- return data ? 1n : 0n;
133
+ if (context.isEnabled(CompatibilityOption.fixedBooleanInJson)) {
134
+ return data;
135
+ }
136
+
137
+ return data ? SQLITE_TRUE : SQLITE_FALSE;
131
138
  } else if (typeof data == 'bigint') {
132
139
  return data;
133
140
  } else if (data instanceof Date) {
@@ -0,0 +1,108 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { SourceTable } from '@powersync/service-core';
3
+ import { HydratedSyncConfig } from '@powersync/service-sync-rules';
4
+ import { MongoManager } from './MongoManager.js';
5
+ import { MongoReplicationStreamContext, MongoReplicationStreamItem } from './MongoReplicationStream.js';
6
+
7
+ /**
8
+ * Sync config and source namespace information used to construct a provider for a replication attempt.
9
+ */
10
+ export interface MongoReplicationQueryProviderContext {
11
+ /**
12
+ * Hydrated sync config from which the provider can read its connection-specific options.
13
+ */
14
+ syncConfig: HydratedSyncConfig;
15
+ /**
16
+ * Identifies the source connection whose options and tables this provider handles.
17
+ */
18
+ connectionTag: string;
19
+ /**
20
+ * MongoDB database used to resolve table references without an explicit schema.
21
+ */
22
+ defaultSchema: string;
23
+ }
24
+
25
+ /**
26
+ * Adapts snapshot selection and the stream of source changes for one replication implementation.
27
+ * The MongoDB module owns cursor options, namespaces, BSON parsing, large-event reassembly and durable progress.
28
+ */
29
+ export interface MongoReplicationQueryProvider {
30
+ /**
31
+ * Validate source capabilities before either snapshot queries or streaming cursors are opened.
32
+ * Throw if required capabilities are unavailable. Both the stream and snapshotter may call this hook
33
+ * on the same provider, so validation must be safe to repeat.
34
+ */
35
+ validateSource?(options: { connectionManager: MongoManager; isDocumentDb: boolean }): Promise<void>;
36
+
37
+ /**
38
+ * Call context.open() with any additional stages/pre-image options, then optionally wrap its iterator.
39
+ * Unlike pipeline stages alone, the wrapper can exclude marked events while still forwarding progress.
40
+ * Yield changes and safe progress, including when all data events are excluded.
41
+ * Count excluded complete events in the next progress item's filteredCount, resetting it after each
42
+ * progress item. Preserve the reader's safe resume token rather than advancing past buffered changes.
43
+ * An exit from a filtered set may be represented as a synthetic delete retaining its original key and token.
44
+ * Checkpoint/DDL events must remain ordered and intact. Progress alone must never stand in for a checkpoint.
45
+ * Each call opens a separate iterator, also used for snapshot barriers and resume validation. Keep
46
+ * per-iterator state separate and close the underlying iterator when the wrapper exits.
47
+ */
48
+ openChangeStream(context: MongoReplicationStreamContext): AsyncIterableIterator<MongoReplicationStreamItem>;
49
+
50
+ /**
51
+ * Returns an additional MongoDB find predicate for a physical source table.
52
+ * Return null to snapshot without an additional filter. Snapshot selection must agree with the
53
+ * provider's streaming rules so both paths replicate the same set of documents.
54
+ * Expressions can be supplied as { $expr: ... }. The snapshot query combines the filter with its
55
+ * _id continuation predicate and uses simple collation whenever a filter is supplied.
56
+ */
57
+ getSnapshotFilter(table: SourceTable): mongo.Filter<mongo.Document> | null;
58
+ }
59
+
60
+ /**
61
+ * Creates a provider shared by a replication attempt's change stream, snapshotter and source probes.
62
+ */
63
+ export type MongoReplicationQueryProviderFactory = (
64
+ context: MongoReplicationQueryProviderContext
65
+ ) => MongoReplicationQueryProvider;
66
+
67
+ /**
68
+ * Uses the standard MongoDB reader and snapshot selection without additional filtering or event translation.
69
+ */
70
+ export const DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER: MongoReplicationQueryProvider = {
71
+ getSnapshotFilter: () => null,
72
+ openChangeStream: ({ open }) => open({})
73
+ };
74
+
75
+ /**
76
+ * Places provider stages after namespace selection and before optional large-event splitting.
77
+ * This lets provider stages inspect complete events; the shared reader reassembles any resulting fragments.
78
+ */
79
+ export function buildMongoChangeStreamPipeline(options: {
80
+ /**
81
+ * Shared reader options, including resume position, stream scope and requested document images.
82
+ */
83
+ streamOptions: mongo.ChangeStreamOptions & mongo.Document;
84
+ /**
85
+ * Namespace predicate restricting the stream to the selected source collections and checkpoint events.
86
+ */
87
+ namespaceMatch: mongo.Document;
88
+ /**
89
+ * Additional aggregation stages supplied by the provider, in execution order.
90
+ */
91
+ pipelineStages: mongo.Document[];
92
+ /**
93
+ * Append MongoDB's large-event split stage. Disabled for DocumentDB by the shared reader.
94
+ */
95
+ includeSplitLargeEvent: boolean;
96
+ }): mongo.Document[] {
97
+ const pipeline: mongo.Document[] = [
98
+ { $changeStream: options.streamOptions },
99
+ { $match: options.namespaceMatch },
100
+ ...options.pipelineStages
101
+ ];
102
+
103
+ if (options.includeSplitLargeEvent) {
104
+ pipeline.push({ $changeStreamSplitLargeEvent: {} });
105
+ }
106
+
107
+ return pipeline;
108
+ }
@@ -0,0 +1,201 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { Logger, ReplicationAssertionError } from '@powersync/lib-services-framework';
3
+ import { buildMongoChangeStreamPipeline, MongoReplicationQueryProvider } from './MongoReplicationQueryProvider.js';
4
+ import {
5
+ ChangeStreamBatch,
6
+ parseChangeDocument,
7
+ ProjectedChangeStreamDocument,
8
+ rawChangeStream,
9
+ RawChangeStreamOptions
10
+ } from './RawChangeStream.js';
11
+
12
+ /**
13
+ * Ordered adapter output. Progress advances source recovery, not the client-visible checkpoint.
14
+ */
15
+ export type MongoReplicationStreamItem =
16
+ | {
17
+ type: 'change';
18
+ event: ProjectedChangeStreamDocument;
19
+ /**
20
+ * More envelopes remain in the received batch, including ones an adapter may exclude.
21
+ */
22
+ hasBufferedChanges: boolean;
23
+ }
24
+ | {
25
+ type: 'progress';
26
+ /**
27
+ * Covers only complete events already delivered or intentionally excluded by the adapter.
28
+ */
29
+ resumeToken: mongo.ResumeToken;
30
+ /**
31
+ * Excluded complete events since the previous progress item; reset after every boundary.
32
+ */
33
+ filteredCount: number;
34
+ };
35
+
36
+ export interface MongoReplicationStreamOptions {
37
+ /**
38
+ * Inserted after namespace selection and before the final large-event split stage.
39
+ */
40
+ pipelineStages?: mongo.Document[];
41
+ /**
42
+ * Request pre-images without overriding the reader's post-image, resume or scope options.
43
+ */
44
+ imageOptions?: Pick<mongo.ChangeStreamOptions, 'fullDocumentBeforeChange'>;
45
+ }
46
+
47
+ export interface MongoReplicationStreamContext {
48
+ /**
49
+ * Open the source reader with the current position, namespace, deadlines and cancellation already bound.
50
+ * The reader returns parsed, reassembled changes followed by safe progress boundaries. A provider can
51
+ * add stages here and wrap the result to discard marked events or turn filter exits into deletes.
52
+ * Supplying this callback keeps cursor setup shared with snapshot barriers and resume validation.
53
+ *
54
+ * The default provider simply returns open({}). Wrappers must preserve event/progress ordering and
55
+ * close the returned iterator on completion, cancellation or failure (for await handles this).
56
+ */
57
+ open(options: MongoReplicationStreamOptions): AsyncIterableIterator<MongoReplicationStreamItem>;
58
+ }
59
+
60
+ /**
61
+ * Shared opening path for ongoing replication, snapshot barriers and resume validation.
62
+ */
63
+ export function openMongoReplicationStream({
64
+ db,
65
+ queryProvider,
66
+ namespaceFilter,
67
+ isDocumentDb,
68
+ usePostImages,
69
+ position,
70
+ skipInitialTimestamp = false,
71
+ options,
72
+ onBatch
73
+ }: {
74
+ db: mongo.Db;
75
+ queryProvider: MongoReplicationQueryProvider;
76
+ namespaceFilter: { $match: mongo.Document; multipleDatabases: boolean };
77
+ isDocumentDb: boolean;
78
+ usePostImages: boolean;
79
+ position: { resumeAfter?: mongo.ResumeToken | null; startAfter?: mongo.Timestamp | null } | null;
80
+ /**
81
+ * Legacy streaming positions exclude the initial timestamp; snapshot barrier probes must include it.
82
+ */
83
+ skipInitialTimestamp?: boolean;
84
+ options: RawChangeStreamOptions;
85
+ onBatch?: (batch: ChangeStreamBatch) => Disposable | void;
86
+ }): AsyncIterableIterator<MongoReplicationStreamItem> {
87
+ const open: MongoReplicationStreamContext['open'] = ({ pipelineStages = [], imageOptions = {} }) => {
88
+ const clusterScope = isDocumentDb || namespaceFilter.multipleDatabases;
89
+ const streamOptions: mongo.Document = {
90
+ ...imageOptions,
91
+ fullDocument: !isDocumentDb && usePostImages ? 'required' : 'updateLookup',
92
+ ...(!isDocumentDb ? { showExpandedEvents: true } : {}),
93
+ ...(clusterScope ? { allChangesForCluster: true } : {})
94
+ };
95
+ // Only one resume option is legal. Legacy MongoDB snapshot positions may contain just a timestamp;
96
+ // a fresh DocumentDB stream has neither and opens from now.
97
+ if (position?.resumeAfter != null) {
98
+ streamOptions.resumeAfter = position.resumeAfter;
99
+ } else if (position?.startAfter != null) {
100
+ streamOptions.startAtOperationTime = position.startAfter;
101
+ }
102
+ const pipeline = buildMongoChangeStreamPipeline({
103
+ streamOptions,
104
+ namespaceMatch: namespaceFilter.$match,
105
+ pipelineStages,
106
+ includeSplitLargeEvent: !isDocumentDb
107
+ });
108
+ return readMongoReplicationStream({
109
+ batches: rawChangeStream(clusterScope ? db.client.db('admin') : db, pipeline, options),
110
+ defaultSchema: db.databaseName,
111
+ multipleDatabases: namespaceFilter.multipleDatabases,
112
+ // An exact token can fall inside a transaction. Later events may share its timestamp and must
113
+ // still be delivered. Timestamp deduplication applies only to legacy positions without tokens.
114
+ startAfter: skipInitialTimestamp && position?.resumeAfter == null ? position?.startAfter : undefined,
115
+ signal: options.signal,
116
+ logger: options.logger,
117
+ onBatch
118
+ });
119
+ };
120
+ return queryProvider.openChangeStream({ open });
121
+ }
122
+
123
+ /**
124
+ * Parse envelopes once, retaining raw BSON row bodies and withholding progress during split reassembly.
125
+ */
126
+ export async function* readMongoReplicationStream({
127
+ batches,
128
+ defaultSchema,
129
+ multipleDatabases,
130
+ startAfter,
131
+ signal,
132
+ logger,
133
+ onBatch
134
+ }: {
135
+ batches: AsyncIterable<ChangeStreamBatch>;
136
+ defaultSchema: string;
137
+ multipleDatabases: boolean;
138
+ startAfter?: mongo.Timestamp | null;
139
+ signal?: AbortSignal;
140
+ logger?: Logger;
141
+ /**
142
+ * Transport accounting happens before parsing/filtering, including partial fragments and empty batches.
143
+ */
144
+ onBatch?: (batch: ChangeStreamBatch) => Disposable | void;
145
+ }): AsyncGenerator<MongoReplicationStreamItem> {
146
+ let splitDocument: ProjectedChangeStreamDocument | null = null;
147
+ let flexDbNameWorkaroundLogged = false;
148
+ for await (const batch of batches) {
149
+ signal?.throwIfAborted();
150
+ // The optional scope measures batch processing, excluding the next network wait.
151
+ using batchScope = onBatch?.(batch) || undefined;
152
+ for (const [index, rawEvent] of batch.events.entries()) {
153
+ signal?.throwIfAborted();
154
+ let event = parseChangeDocument(rawEvent);
155
+ const fragment = event.splitEvent;
156
+ if (fragment != null) {
157
+ // A retry can resume after a received fragment inside the raw reader. Keep the partial event in
158
+ // memory across batches/retries, but never expose its token as a durable recovery boundary.
159
+ const expectedFragment = splitDocument == null ? 1 : splitDocument.splitEvent!.fragment + 1;
160
+ if (
161
+ fragment.fragment != expectedFragment ||
162
+ fragment.fragment > fragment.of ||
163
+ (splitDocument != null && fragment.of != splitDocument.splitEvent!.of)
164
+ ) {
165
+ throw new ReplicationAssertionError(`Unexpected splitEvent: ${JSON.stringify(fragment)}`);
166
+ }
167
+ splitDocument = splitDocument == null ? event : Object.assign(splitDocument, event);
168
+ if (fragment.fragment != fragment.of) continue;
169
+ event = splitDocument;
170
+ splitDocument = null;
171
+ } else if (splitDocument != null) {
172
+ throw new ReplicationAssertionError(`Incomplete splitEvent: ${JSON.stringify(splitDocument.splitEvent)}`);
173
+ }
174
+
175
+ if (startAfter != null && event.clusterTime?.lte(startAfter)) continue;
176
+
177
+ if (
178
+ !multipleDatabases &&
179
+ 'ns' in event &&
180
+ event.ns.db != defaultSchema &&
181
+ event.ns.db.endsWith(`_${defaultSchema}`)
182
+ ) {
183
+ // Atlas Flex can prefix the database name on events recorded while replication was paused.
184
+ // Normalize before adapters inspect namespaces, just as the ordinary replication loop does.
185
+ const originalDatabase = event.ns.db;
186
+ event.ns.db = defaultSchema;
187
+ if (!flexDbNameWorkaroundLogged) {
188
+ flexDbNameWorkaroundLogged = true;
189
+ logger?.warn(`Incorrect DB name in change stream: ${originalDatabase}. Changed to ${defaultSchema}.`);
190
+ }
191
+ }
192
+ yield { type: 'change', event, hasBufferedChanges: index < batch.events.length - 1 };
193
+ }
194
+ if (splitDocument == null) {
195
+ yield { type: 'progress', resumeToken: batch.resumeToken, filteredCount: 0 };
196
+ }
197
+ }
198
+ if (splitDocument != null) {
199
+ throw new ReplicationAssertionError(`Incomplete splitEvent: ${JSON.stringify(splitDocument.splitEvent)}`);
200
+ }
201
+ }
@@ -15,12 +15,19 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
15
15
  private lastCursor: mongo.FindCursor | null = null;
16
16
  private collection: mongo.Collection;
17
17
  private batchSize: number;
18
+ private readonly filter: mongo.Filter<mongo.Document> | null;
18
19
 
19
- public constructor(options: { collection: mongo.Collection; batchSize: number; key?: Uint8Array | null }) {
20
+ public constructor(options: {
21
+ collection: mongo.Collection;
22
+ batchSize: number;
23
+ key?: Uint8Array | null;
24
+ filter?: mongo.Filter<mongo.Document> | null;
25
+ }) {
20
26
  this.lastKey = options.key ? bson.deserialize(options.key, { useBigInt64: true })._id : null;
21
27
  this.lastCursor = null;
22
28
  this.collection = options.collection;
23
29
  this.batchSize = options.batchSize;
30
+ this.filter = options.filter ?? null;
24
31
  }
25
32
 
26
33
  async nextChunk(): Promise<
@@ -39,8 +46,14 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
39
46
  // any parsing as an operator.
40
47
  // Starting in MongoDB 5.0, this filter can use the _id index. Source:
41
48
  // https://www.mongodb.com/docs/manual/release-notes/5.0/#general-aggregation-improvements
42
- const filter: mongo.Filter<mongo.Document> =
49
+ const continuationFilter: mongo.Filter<mongo.Document> =
43
50
  this.lastKey == null ? {} : { $expr: { $gt: ['$_id', { $literal: this.lastKey }] } };
51
+ const filter: mongo.Filter<mongo.Document> =
52
+ this.filter == null
53
+ ? continuationFilter
54
+ : this.lastKey == null
55
+ ? this.filter
56
+ : { $and: [this.filter, continuationFilter] };
44
57
  cursor = this.collection.find(filter, {
45
58
  readConcern: 'majority',
46
59
  limit: this.batchSize,
@@ -48,7 +61,10 @@ export class ChunkedSnapshotQuery implements AsyncDisposable {
48
61
  // See https://github.com/mongodb/node-mongodb-native/pull/4580
49
62
  batchSize: this.batchSize + 1,
50
63
  sort: { _id: 1 },
51
- raw: true
64
+ raw: true,
65
+ // Replication predicates have stable binary semantics independent of a
66
+ // collection's default collation.
67
+ ...(this.filter == null ? {} : { collation: { locale: 'simple' } })
52
68
  });
53
69
  newCursor = true;
54
70
  }