Skip to main content

risingwave_connector/sink/
mongodb.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{BTreeMap, HashMap};
16use std::ops::Deref;
17use std::sync::LazyLock;
18
19use anyhow::anyhow;
20use futures::TryFutureExt;
21use futures::future::{TryJoinAll, try_join_all};
22use futures::prelude::TryFuture;
23use itertools::Itertools;
24use mongodb::bson::{Array, Bson, Document, bson, doc};
25use mongodb::{Client, Namespace};
26use risingwave_common::array::{Op, RowRef, StreamChunk};
27use risingwave_common::catalog::Schema;
28use risingwave_common::log::LogSuppressor;
29use risingwave_common::row::Row;
30use risingwave_common::types::ScalarRefImpl;
31use serde::Deserialize;
32use serde_with::{DisplayFromStr, serde_as};
33use thiserror_ext::AsReport;
34use with_options::WithOptions;
35
36use super::encoder::BsonEncoder;
37use super::log_store::DeliveryFutureManagerAddFuture;
38use super::writer::{
39    AsyncTruncateLogSinkerOf, AsyncTruncateSinkWriter, AsyncTruncateSinkWriterExt,
40};
41use crate::connector_common::MongodbCommon;
42use crate::deserialize_bool_from_string;
43use crate::enforce_secret::EnforceSecret;
44use crate::sink::encoder::RowEncoder;
45use crate::sink::{
46    Result, SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, Sink, SinkError, SinkParam,
47    SinkWriterParam,
48};
49
50mod send_bulk_write_command_future {
51    use core::future::Future;
52
53    use anyhow::anyhow;
54    use mongodb::Database;
55    use mongodb::bson::Document;
56
57    use crate::sink::{Result, SinkError};
58
59    pub(super) type SendBulkWriteCommandFuture = impl Future<Output = Result<()>> + 'static;
60
61    #[define_opaque(SendBulkWriteCommandFuture)]
62    pub(super) fn send_bulk_write_commands(
63        db: Database,
64        upsert: Option<Document>,
65        delete: Option<Document>,
66    ) -> SendBulkWriteCommandFuture {
67        async move {
68            if let Some(upsert) = upsert {
69                send_bulk_write_command(db.clone(), upsert).await?;
70            }
71            if let Some(delete) = delete {
72                send_bulk_write_command(db, delete).await?;
73            }
74            Ok(())
75        }
76    }
77
78    async fn send_bulk_write_command(db: Database, command: Document) -> Result<()> {
79        let result = db.run_command(command).await.map_err(|err| {
80            SinkError::Mongodb(anyhow!(err).context(format!(
81                "sending bulk write command failed, database: {}",
82                db.name()
83            )))
84        })?;
85
86        if let Ok(ok) = result.get_i32("ok")
87            && ok != 1
88        {
89            return Err(SinkError::Mongodb(anyhow!("bulk write write errors")));
90        }
91
92        if let Ok(write_errors) = result.get_array("writeErrors") {
93            return Err(SinkError::Mongodb(anyhow!(
94                "bulk write respond with write errors: {:?}",
95                write_errors,
96            )));
97        }
98
99        if let Ok(write_concern_error) = result.get_array("writeConcernError") {
100            return Err(SinkError::Mongodb(anyhow!(
101                "bulk write respond with write errors: {:?}",
102                write_concern_error,
103            )));
104        }
105
106        Ok(())
107    }
108}
109
110pub const MONGODB_SINK: &str = "mongodb";
111const MONGODB_SEND_FUTURE_BUFFER_MAX_SIZE: usize = 4096;
112
113pub const MONGODB_PK_NAME: &str = "_id";
114
115static LOG_SUPPRESSOR: LazyLock<LogSuppressor> = LazyLock::new(LogSuppressor::default);
116
117const fn _default_bulk_write_max_entries() -> usize {
118    1024
119}
120#[serde_as]
121#[derive(Clone, Debug, Deserialize, WithOptions)]
122pub struct MongodbConfig {
123    #[serde(flatten)]
124    pub common: MongodbCommon,
125
126    pub r#type: String, // accept "append-only" or "upsert"
127
128    /// The dynamic collection name where data should be sunk to. If specified, the field value will be used
129    /// as the collection name. The collection name format is same as `collection.name`. If the field value is
130    /// null or an empty string, then the `collection.name` will be used as a fallback destination.
131    #[serde(rename = "collection.name.field")]
132    pub collection_name_field: Option<String>,
133
134    /// Controls whether the field value of `collection.name.field` should be dropped when sinking.
135    /// Set this option to true to avoid the duplicate values of `collection.name.field` being written to the
136    /// result collection.
137    #[serde(
138        default,
139        deserialize_with = "deserialize_bool_from_string",
140        rename = "collection.name.field.drop"
141    )]
142    pub drop_collection_name_field: bool,
143
144    /// The maximum entries will accumulate before performing the bulk write, defaults to 1024.
145    #[serde(
146        rename = "mongodb.bulk_write.max_entries",
147        default = "_default_bulk_write_max_entries"
148    )]
149    #[serde_as(as = "DisplayFromStr")]
150    #[deprecated]
151    pub bulk_write_max_entries: usize,
152
153    #[serde(flatten)]
154    pub unknown_fields: std::collections::HashMap<String, String>,
155}
156
157crate::impl_sink_unknown_fields!(MongodbConfig);
158
159impl EnforceSecret for MongodbConfig {
160    fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
161        MongodbCommon::enforce_one(prop)
162    }
163}
164
165impl MongodbConfig {
166    pub fn from_btreemap(properties: BTreeMap<String, String>) -> crate::sink::Result<Self> {
167        let config =
168            serde_json::from_value::<MongodbConfig>(serde_json::to_value(properties).unwrap())
169                .map_err(|e| SinkError::Config(anyhow!(e)))?;
170        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
171            return Err(SinkError::Config(anyhow!(
172                "`{}` must be {}, or {}",
173                SINK_TYPE_OPTION,
174                SINK_TYPE_APPEND_ONLY,
175                SINK_TYPE_UPSERT
176            )));
177        }
178        Ok(config)
179    }
180}
181
182/// An async-drop style `Guard` for `mongodb::Client`. Use this guard to hold a client,
183/// the `client::shutdown` is called in an async manner when the guard is dropped.
184/// Please be aware this is a "best effort" style shutdown, which may not be successful if the
185/// tokio runtime is in the process of terminating. However, the server-side resources will be
186/// cleaned up eventually due to the session expiration.
187/// see [this issue](https://github.com/mongodb/mongo-rust-driver/issues/719) for more information
188struct ClientGuard {
189    _tx: tokio::sync::oneshot::Sender<()>,
190    client: Client,
191}
192
193impl ClientGuard {
194    fn new(name: String, client: Client) -> Self {
195        let client_copy = client.clone();
196        let (_tx, rx) = tokio::sync::oneshot::channel::<()>();
197        tokio::spawn(async move {
198            tracing::debug!(%name, "waiting for client to shut down");
199            let _ = rx.await;
200            tracing::debug!(%name, "sender dropped now calling client's shutdown");
201            // shutdown may stuck if the resources created by client are not dropped at this point.
202            // As recommended by [shutdown](https://docs.rs/mongodb/2.8.2/mongodb/struct.Client.html#method.shutdown)
203            // documentation, we should make our resources usage shorter-lived than the client. So if this happens,
204            // there are some programming error in our code.
205            client_copy.shutdown().await;
206            tracing::debug!(%name, "client shutdown succeeded");
207        });
208        Self { _tx, client }
209    }
210}
211
212impl Deref for ClientGuard {
213    type Target = Client;
214
215    fn deref(&self) -> &Self::Target {
216        &self.client
217    }
218}
219
220#[derive(Debug)]
221pub struct MongodbSink {
222    pub config: MongodbConfig,
223    param: SinkParam,
224    schema: Schema,
225    pk_indices: Vec<usize>,
226    is_append_only: bool,
227}
228
229impl EnforceSecret for MongodbSink {
230    fn enforce_secret<'a>(
231        prop_iter: impl Iterator<Item = &'a str>,
232    ) -> crate::sink::ConnectorResult<()> {
233        for prop in prop_iter {
234            MongodbConfig::enforce_one(prop)?;
235        }
236        Ok(())
237    }
238}
239
240impl MongodbSink {
241    pub fn new(param: SinkParam) -> Result<Self> {
242        let config = MongodbConfig::from_btreemap(param.properties.clone())?;
243        let pk_indices = param.downstream_pk_or_empty();
244        let is_append_only = param.sink_type.is_append_only();
245        let schema = param.schema();
246        Ok(Self {
247            config,
248            param,
249            schema,
250            pk_indices,
251            is_append_only,
252        })
253    }
254}
255
256impl TryFrom<SinkParam> for MongodbSink {
257    type Error = SinkError;
258
259    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
260        MongodbSink::new(param)
261    }
262}
263
264impl Sink for MongodbSink {
265    type LogSinker = AsyncTruncateLogSinkerOf<MongodbSinkWriter>;
266
267    const SINK_NAME: &'static str = MONGODB_SINK;
268
269    crate::impl_validate_sink_unknown_fields!();
270
271    async fn validate(&self) -> Result<()> {
272        if !self.is_append_only {
273            if self.pk_indices.is_empty() {
274                return Err(SinkError::Config(anyhow!(
275                    "Primary key not defined for upsert mongodb sink (please define in `primary_key` field)"
276                )));
277            }
278
279            // checking if there is a non-pk field's name is `_id`
280            if self
281                .schema
282                .fields
283                .iter()
284                .enumerate()
285                .any(|(i, field)| !self.pk_indices.contains(&i) && field.name == MONGODB_PK_NAME)
286            {
287                return Err(SinkError::Config(anyhow!(
288                    "_id field must be the sink's primary key, but a non primary key field name is _id",
289                )));
290            }
291
292            // assume the sink's pk is (a, b) and then the data written to mongodb will be
293            // { "_id": {"a": 1, "b": 2}, "a": 1, "b": 2, ... }
294            // you can see that the compound pk (a, b) is turned into an Object {"a": 1, "b": 2}
295            // and the each pk field is become as a field of the document
296            // but if the sink's pk is (_id, b) and the data will be:
297            // { "_id": {"_id": 1, "b": 2}, "b": 2, ... }
298            // in this case, the original _id field of the compound pk has been overridden
299            // we should consider this is a schema error
300            if self.pk_indices.len() > 1
301                && self
302                    .pk_indices
303                    .iter()
304                    .map(|&idx| self.schema.fields[idx].name.as_str())
305                    .any(|field| field == MONGODB_PK_NAME)
306            {
307                return Err(SinkError::Config(anyhow!(
308                    "primary key fields must not contain a field named _id"
309                )));
310            }
311        }
312
313        if let Err(err) = self.config.common.collection_name.parse::<Namespace>() {
314            return Err(SinkError::Config(anyhow!(err).context(format!(
315                "invalid collection.name {}",
316                self.config.common.collection_name
317            ))));
318        }
319
320        // checking reachability
321        let client = self.config.common.build_client().await?;
322        let client = ClientGuard::new(self.param.sink_name.clone(), client);
323        client
324            .database("admin")
325            .run_command(doc! {"hello":1})
326            .await
327            .map_err(|err| {
328                SinkError::Mongodb(anyhow!(err).context("failed to send hello command to mongodb"))
329            })?;
330
331        if self.config.drop_collection_name_field && self.config.collection_name_field.is_none() {
332            return Err(SinkError::Config(anyhow!(
333                "collection.name.field must be specified when collection.name.field.drop is enabled"
334            )));
335        }
336
337        // checking dynamic collection name settings
338        if let Some(coll_field) = &self.config.collection_name_field {
339            let fields = self.schema.fields();
340
341            let coll_field_index = fields
342                .iter()
343                .enumerate()
344                .find_map(|(index, field)| {
345                    if &field.name == coll_field {
346                        Some(index)
347                    } else {
348                        None
349                    }
350                })
351                .ok_or(SinkError::Config(anyhow!(
352                    "collection.name.field {} not found",
353                    coll_field
354                )))?;
355
356            if fields[coll_field_index].data_type() != risingwave_common::types::DataType::Varchar {
357                return Err(SinkError::Config(anyhow!(
358                    "the type of collection.name.field {} must be varchar",
359                    coll_field
360                )));
361            }
362
363            if !self.is_append_only && self.pk_indices.contains(&coll_field_index) {
364                return Err(SinkError::Config(anyhow!(
365                    "collection.name.field {} must not be equal to the primary key field",
366                    coll_field
367                )));
368            }
369        }
370
371        Ok(())
372    }
373
374    async fn new_log_sinker(&self, writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
375        Ok(MongodbSinkWriter::new(
376            format!("{}-{}", writer_param.executor_id, self.param.sink_name),
377            self.config.clone(),
378            self.schema.clone(),
379            self.pk_indices.clone(),
380            self.is_append_only,
381        )
382        .await?
383        .into_log_sinker(MONGODB_SEND_FUTURE_BUFFER_MAX_SIZE))
384    }
385}
386
387use send_bulk_write_command_future::*;
388
389pub struct MongodbSinkWriter {
390    pub config: MongodbConfig,
391    payload_writer: MongodbPayloadWriter,
392    is_append_only: bool,
393}
394
395impl MongodbSinkWriter {
396    pub async fn new(
397        name: String,
398        config: MongodbConfig,
399        schema: Schema,
400        pk_indices: Vec<usize>,
401        is_append_only: bool,
402    ) -> Result<Self> {
403        let client = config.common.build_client().await?;
404
405        let default_namespace =
406            config
407                .common
408                .collection_name
409                .parse()
410                .map_err(|err: mongodb::error::Error| {
411                    SinkError::Mongodb(anyhow!(err).context("parsing default namespace failed"))
412                })?;
413
414        let coll_name_field_index =
415            config
416                .collection_name_field
417                .as_ref()
418                .and_then(|coll_name_field| {
419                    schema
420                        .names_str()
421                        .iter()
422                        .position(|&name| coll_name_field == name)
423                });
424
425        let col_indices = if let Some(coll_name_field_index) = coll_name_field_index
426            && config.drop_collection_name_field
427        {
428            (0..schema.fields.len())
429                .filter(|idx| *idx != coll_name_field_index)
430                .collect_vec()
431        } else {
432            (0..schema.fields.len()).collect_vec()
433        };
434
435        let row_encoder = BsonEncoder::new(schema.clone(), Some(col_indices), pk_indices.clone());
436
437        let payload_writer = MongodbPayloadWriter::new(
438            schema,
439            pk_indices,
440            default_namespace,
441            coll_name_field_index,
442            ClientGuard::new(name, client),
443            row_encoder,
444        );
445
446        Ok(Self {
447            config,
448            payload_writer,
449            is_append_only,
450        })
451    }
452
453    fn append(&mut self, chunk: StreamChunk) -> Result<TryJoinAll<SendBulkWriteCommandFuture>> {
454        let mut insert_builder: HashMap<MongodbNamespace, InsertCommandBuilder> = HashMap::new();
455        for (op, row) in chunk.rows() {
456            if op != Op::Insert {
457                if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
458                    tracing::warn!(
459                        suppressed_count,
460                        ?op,
461                        ?row,
462                        "non-insert op received in append-only mode"
463                    );
464                }
465                continue;
466            }
467            self.payload_writer.append(&mut insert_builder, row)?;
468        }
469        Ok(self.payload_writer.flush_insert(insert_builder))
470    }
471
472    fn upsert(&mut self, chunk: StreamChunk) -> Result<TryJoinAll<SendBulkWriteCommandFuture>> {
473        let mut upsert_builder: HashMap<MongodbNamespace, UpsertCommandBuilder> = HashMap::new();
474        for (op, row) in chunk.rows() {
475            if op == Op::UpdateDelete {
476                // we should ignore the `UpdateDelete` in upsert mode
477                continue;
478            }
479            self.payload_writer.upsert(&mut upsert_builder, op, row)?;
480        }
481        Ok(self.payload_writer.flush_upsert(upsert_builder))
482    }
483}
484
485pub type MongodbSinkDeliveryFuture = impl TryFuture<Ok = (), Error = SinkError> + Unpin + 'static;
486
487impl AsyncTruncateSinkWriter for MongodbSinkWriter {
488    type DeliveryFuture = MongodbSinkDeliveryFuture;
489
490    #[define_opaque(MongodbSinkDeliveryFuture)]
491    async fn write_chunk<'a>(
492        &'a mut self,
493        chunk: StreamChunk,
494        mut add_future: DeliveryFutureManagerAddFuture<'a, Self::DeliveryFuture>,
495    ) -> Result<()> {
496        let futures = if self.is_append_only {
497            self.append(chunk)?
498        } else {
499            self.upsert(chunk)?
500        };
501        add_future
502            .add_future_may_await(futures.map_ok(|_: Vec<()>| ()))
503            .await?;
504        Ok(())
505    }
506}
507
508struct InsertCommandBuilder {
509    coll: String,
510    inserts: Array,
511}
512
513impl InsertCommandBuilder {
514    fn new(coll: String) -> Self {
515        Self {
516            coll,
517            inserts: Array::new(),
518        }
519    }
520
521    fn append(&mut self, row: Document) {
522        self.inserts.push(Bson::Document(row));
523    }
524
525    fn build(self) -> Document {
526        doc! {
527            "insert": self.coll,
528            "ordered": true,
529            "documents": self.inserts,
530        }
531    }
532}
533
534struct UpsertCommandBuilder {
535    coll: String,
536    updates: Array,
537    deletes: HashMap<Vec<u8>, Document>,
538}
539
540impl UpsertCommandBuilder {
541    fn new(coll: String) -> Self {
542        Self {
543            coll,
544            updates: Array::new(),
545            deletes: HashMap::new(),
546        }
547    }
548
549    fn add_upsert(&mut self, pk: Document, row: Document) -> Result<()> {
550        let pk_data = mongodb::bson::to_vec(&pk).map_err(|err| {
551            SinkError::Mongodb(anyhow!(err).context("cannot serialize primary key"))
552        })?;
553        // under same pk, if the record currently being upserted was marked for deletion previously, we should
554        // revert the deletion, otherwise, the upserting record may be accidentally deleted.
555        // see https://github.com/risingwavelabs/risingwave/pull/17102#discussion_r1630684160 for more information.
556        self.deletes.remove(&pk_data);
557
558        self.updates.push(bson!( {
559            "q": pk,
560            "u": bson!( {
561                "$set": row,
562            }),
563            "upsert": true,
564            "multi": false,
565        }));
566
567        Ok(())
568    }
569
570    fn add_delete(&mut self, pk: Document) -> Result<()> {
571        let pk_data = mongodb::bson::to_vec(&pk).map_err(|err| {
572            SinkError::Mongodb(anyhow!(err).context("cannot serialize primary key"))
573        })?;
574        self.deletes.insert(pk_data, pk);
575        Ok(())
576    }
577
578    fn build(self) -> (Option<Document>, Option<Document>) {
579        let (mut upsert_document, mut delete_document) = (None, None);
580        if !self.updates.is_empty() {
581            upsert_document = Some(doc! {
582                "update": self.coll.clone(),
583                "ordered": true,
584                "updates": self.updates,
585            });
586        }
587        if !self.deletes.is_empty() {
588            let deletes = self
589                .deletes
590                .into_values()
591                .map(|pk| {
592                    bson!({
593                        "q": pk,
594                        "limit": 1,
595                    })
596                })
597                .collect::<Array>();
598
599            delete_document = Some(doc! {
600                "delete": self.coll,
601                "ordered": true,
602                "deletes": deletes,
603            });
604        }
605        (upsert_document, delete_document)
606    }
607}
608
609type MongodbNamespace = (String, String);
610
611// In the future, we may build the payload into RawBSON to gain a better performance.
612// The current API (mongodb-2.8.2) lacks the support of writing RawBSON.
613struct MongodbPayloadWriter {
614    schema: Schema,
615    pk_indices: Vec<usize>,
616    default_namespace: Namespace,
617    coll_name_field_index: Option<usize>,
618    client: ClientGuard,
619    row_encoder: BsonEncoder,
620}
621
622impl MongodbPayloadWriter {
623    fn new(
624        schema: Schema,
625        pk_indices: Vec<usize>,
626        default_namespace: Namespace,
627        coll_name_field_index: Option<usize>,
628        client: ClientGuard,
629        row_encoder: BsonEncoder,
630    ) -> Self {
631        Self {
632            schema,
633            pk_indices,
634            default_namespace,
635            coll_name_field_index,
636            client,
637            row_encoder,
638        }
639    }
640
641    fn extract_namespace_from_row_ref(&self, row: RowRef<'_>) -> MongodbNamespace {
642        let ns = self.coll_name_field_index.and_then(|coll_name_field_index| {
643            match row.datum_at(coll_name_field_index) {
644                Some(ScalarRefImpl::Utf8(v)) => match v.parse::<Namespace>() {
645                    Ok(ns) => Some(ns),
646                    Err(err) => {
647                        if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
648                            tracing::warn!(
649                                suppressed_count,
650                                error = %err.as_report(),
651                                collection_name = %v,
652                                "parsing collection name failed, fallback to use default collection.name"
653                            );
654                        }
655                        None
656                    }
657                },
658                _ => {
659                    if let Ok(suppressed_count) = LOG_SUPPRESSOR.check() {
660                        tracing::warn!(
661                            suppressed_count,
662                            "the value of collection.name.field is null, fallback to use default collection.name"
663                        );
664                    }
665                    None
666                }
667            }
668        });
669        match ns {
670            Some(ns) => (ns.db, ns.coll),
671            None => (
672                self.default_namespace.db.clone(),
673                self.default_namespace.coll.clone(),
674            ),
675        }
676    }
677
678    fn append(
679        &mut self,
680        insert_builder: &mut HashMap<MongodbNamespace, InsertCommandBuilder>,
681        row: RowRef<'_>,
682    ) -> Result<()> {
683        let document = self.row_encoder.encode(row)?;
684        let ns = self.extract_namespace_from_row_ref(row);
685        let coll = ns.1.clone();
686
687        insert_builder
688            .entry(ns)
689            .or_insert_with(|| InsertCommandBuilder::new(coll))
690            .append(document);
691        Ok(())
692    }
693
694    fn upsert(
695        &mut self,
696        upsert_builder: &mut HashMap<MongodbNamespace, UpsertCommandBuilder>,
697        op: Op,
698        row: RowRef<'_>,
699    ) -> Result<()> {
700        let mut document = self.row_encoder.encode(row)?;
701        let ns = self.extract_namespace_from_row_ref(row);
702        let coll = ns.1.clone();
703
704        let pk = self.row_encoder.construct_pk(row);
705
706        // Specify the primary key (_id) for the MongoDB collection if the user does not provide one.
707        if self.pk_indices.len() > 1
708            || self.schema.fields[self.pk_indices[0]].name != MONGODB_PK_NAME
709        {
710            // compound pk should not have a field named `_id`
711            document.insert(MONGODB_PK_NAME, pk.clone());
712        }
713
714        let pk = doc! {MONGODB_PK_NAME: pk};
715        match op {
716            Op::Insert | Op::UpdateInsert => upsert_builder
717                .entry(ns)
718                .or_insert_with(|| UpsertCommandBuilder::new(coll))
719                .add_upsert(pk, document)?,
720            Op::UpdateDelete => (),
721            Op::Delete => upsert_builder
722                .entry(ns)
723                .or_insert_with(|| UpsertCommandBuilder::new(coll))
724                .add_delete(pk)?,
725        }
726        Ok(())
727    }
728
729    fn flush_insert(
730        &self,
731        insert_builder: HashMap<MongodbNamespace, InsertCommandBuilder>,
732    ) -> TryJoinAll<SendBulkWriteCommandFuture> {
733        // TODO try sending bulk-write of each collection concurrently to improve the performance when
734        // `dynamic collection` is enabled. We may need to provide best practice to guide user on setting
735        // the MongoDB driver's connection properties.
736        let futures = insert_builder.into_iter().map(|(ns, builder)| {
737            let db = self.client.database(&ns.0);
738            send_bulk_write_commands(db, Some(builder.build()), None)
739        });
740        try_join_all(futures)
741    }
742
743    fn flush_upsert(
744        &self,
745        upsert_builder: HashMap<MongodbNamespace, UpsertCommandBuilder>,
746    ) -> TryJoinAll<SendBulkWriteCommandFuture> {
747        // TODO try sending bulk-write of each collection concurrently to improve the performance when
748        // `dynamic collection` is enabled. We may need to provide best practice to guide user on setting
749        // the MongoDB driver's connection properties.
750        let futures = upsert_builder.into_iter().map(|(ns, builder)| {
751            let (upsert, delete) = builder.build();
752            // we are sending the bulk upsert first because, under same pk, the `Insert` and `UpdateInsert`
753            // should always appear before `Delete`. we have already ignored the `UpdateDelete`
754            // which is useless in upsert mode.
755            let db = self.client.database(&ns.0);
756            send_bulk_write_commands(db, upsert, delete)
757        });
758        try_join_all(futures)
759    }
760}