Skip to main content

faucet_sink_mongodb/
config.rs

1//! MongoDB sink configuration.
2
3use faucet_core::DEFAULT_BATCH_SIZE;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Configuration for the MongoDB sink connector.
9///
10/// # Example
11///
12/// ```
13/// use faucet_sink_mongodb::MongoSinkConfig;
14///
15/// let config = MongoSinkConfig::new(
16///     "mongodb://localhost:27017",
17///     "my_database",
18///     "my_collection",
19/// )
20/// .with_batch_size(1000);
21/// ```
22#[derive(Clone, Serialize, Deserialize, JsonSchema)]
23pub struct MongoSinkConfig {
24    /// MongoDB connection URI (e.g. `mongodb://localhost:27017`).
25    pub connection_uri: String,
26    /// Database name.
27    pub database: String,
28    /// Collection name.
29    pub collection: String,
30    /// Maximum number of documents per `insert_many` call. Defaults to
31    /// [`DEFAULT_BATCH_SIZE`] (1000), which is a good balance for MongoDB's
32    /// per-request limits and round-trip cost.
33    ///
34    /// When `write_batch` is handed a slice larger than `batch_size`, the
35    /// sink re-chunks it into `batch_size` slices and issues one
36    /// `insert_many` per chunk. `batch_size = 0` is the **"no batching"
37    /// sentinel** — the records slice is forwarded as a single
38    /// `insert_many`, no matter how large, so upstream `StreamPage` framing
39    /// flows through untouched.
40    ///
41    /// **Not applied on the exactly-once path**: `write_batch_idempotent`
42    /// always writes the whole page in one transaction (one page = one
43    /// transaction), so re-chunking would break page↔watermark atomicity.
44    #[serde(default = "default_batch_size")]
45    pub batch_size: usize,
46    /// Whether `insert_many` is **ordered**. Default `false` (unordered).
47    ///
48    /// With the MongoDB default of `ordered = true`, the first failing
49    /// document (a duplicate `_id`, a validation error, …) aborts the rest of
50    /// the batch — the documents before it commit, those after are silently
51    /// dropped. Unordered (`false`) instead attempts every document and only
52    /// the genuinely-bad ones fail, so a single poison record can't drop the
53    /// rest of the batch (#78/#20).
54    #[serde(default)]
55    pub ordered: bool,
56    /// Write mode, key columns, and optional delete marker.
57    ///
58    /// `write_mode` defaults to `append` (the `insert_many` fast path). For
59    /// `upsert` / `delete`, `key` must be non-empty — MongoDB being schemaless,
60    /// the key columns become the **match filter** (typically `["_id"]`), not
61    /// table columns. Each upsert is committed with a per-document
62    /// `replace_one(upsert = true)`; each delete with a `delete_one`.
63    #[serde(flatten)]
64    pub write: faucet_core::WriteSpec,
65}
66
67fn default_batch_size() -> usize {
68    DEFAULT_BATCH_SIZE
69}
70
71impl MongoSinkConfig {
72    /// Create a new config with the required connection URI, database, and collection.
73    pub fn new(
74        connection_uri: impl Into<String>,
75        database: impl Into<String>,
76        collection: impl Into<String>,
77    ) -> Self {
78        Self {
79            connection_uri: connection_uri.into(),
80            database: database.into(),
81            collection: collection.into(),
82            batch_size: DEFAULT_BATCH_SIZE,
83            ordered: false,
84            write: faucet_core::WriteSpec::default(),
85        }
86    }
87
88    /// Set whether `insert_many` is ordered (default `false`).
89    pub fn with_ordered(mut self, ordered: bool) -> Self {
90        self.ordered = ordered;
91        self
92    }
93
94    /// Set the maximum number of documents per `insert_many` call.
95    ///
96    /// Pass `0` to opt out of re-chunking — the entire records slice handed
97    /// to `write_batch` is sent in a single `insert_many` call, preserving
98    /// upstream `StreamPage` framing.
99    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
100        self.batch_size = batch_size;
101        self
102    }
103}
104
105impl fmt::Debug for MongoSinkConfig {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.debug_struct("MongoSinkConfig")
108            .field("connection_uri", &"***")
109            .field("database", &self.database)
110            .field("collection", &self.collection)
111            .field("batch_size", &self.batch_size)
112            .field("ordered", &self.ordered)
113            .field("write", &self.write)
114            .finish()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn default_config() {
124        let config = MongoSinkConfig::new("mongodb://localhost:27017", "testdb", "users");
125        assert_eq!(config.database, "testdb");
126        assert_eq!(config.collection, "users");
127        assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
128    }
129
130    #[test]
131    fn batch_size_defaults_to_default_batch_size() {
132        let config = MongoSinkConfig::new("mongodb://localhost:27017", "testdb", "users");
133        assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
134    }
135
136    #[test]
137    fn with_batch_size_overrides_default() {
138        let config = MongoSinkConfig::new("mongodb://localhost:27017", "testdb", "users")
139            .with_batch_size(2000);
140        assert_eq!(config.batch_size, 2000);
141    }
142
143    #[test]
144    fn debug_masks_connection_uri() {
145        let config = MongoSinkConfig::new("mongodb://user:secret@host:27017/db", "testdb", "users");
146        let debug = format!("{config:?}");
147        assert!(debug.contains("***"));
148        assert!(!debug.contains("secret"));
149    }
150
151    #[test]
152    fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
153        let config =
154            MongoSinkConfig::new("mongodb://localhost:27017", "db", "c").with_batch_size(0);
155        assert_eq!(config.batch_size, 0);
156        assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
157    }
158
159    #[test]
160    fn batch_size_above_max_is_rejected_by_validate_batch_size() {
161        let config = MongoSinkConfig::new("mongodb://localhost:27017", "db", "c")
162            .with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
163        assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
164    }
165
166    #[test]
167    fn batch_size_deserializes_from_json() {
168        let json = r#"{
169            "connection_uri": "mongodb://localhost:27017",
170            "database": "db",
171            "collection": "c",
172            "batch_size": 250
173        }"#;
174        let config: MongoSinkConfig = serde_json::from_str(json).unwrap();
175        assert_eq!(config.batch_size, 250);
176    }
177
178    #[test]
179    fn batch_size_defaults_when_absent_in_json() {
180        let json = r#"{
181            "connection_uri": "mongodb://localhost:27017",
182            "database": "db",
183            "collection": "c"
184        }"#;
185        let config: MongoSinkConfig = serde_json::from_str(json).unwrap();
186        assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
187    }
188}