1use 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, #[serde(rename = "collection.name.field")]
132 pub collection_name_field: Option<String>,
133
134 #[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 #[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
182struct 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 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 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 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 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 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 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 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
611struct 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 if self.pk_indices.len() > 1
708 || self.schema.fields[self.pk_indices[0]].name != MONGODB_PK_NAME
709 {
710 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 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 let futures = upsert_builder.into_iter().map(|(ns, builder)| {
751 let (upsert, delete) = builder.build();
752 let db = self.client.database(&ns.0);
756 send_bulk_write_commands(db, upsert, delete)
757 });
758 try_join_all(futures)
759 }
760}