1use faucet_core::DEFAULT_BATCH_SIZE;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7pub use faucet_common_bigquery::BigQueryCredentials;
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13pub struct BigQuerySinkConfig {
14 pub project_id: String,
16 pub dataset_id: String,
18 pub table_id: String,
20 pub auth: BigQueryCredentials,
23 #[serde(default = "default_batch_size")]
38 pub batch_size: usize,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub insert_id_field: Option<String>,
48 #[serde(flatten)]
55 pub write: faucet_core::WriteSpec,
56}
57
58fn default_batch_size() -> usize {
59 DEFAULT_BATCH_SIZE
60}
61
62impl BigQuerySinkConfig {
63 pub fn new(
65 project_id: impl Into<String>,
66 dataset_id: impl Into<String>,
67 table_id: impl Into<String>,
68 credentials: BigQueryCredentials,
69 ) -> Self {
70 Self {
71 project_id: project_id.into(),
72 dataset_id: dataset_id.into(),
73 table_id: table_id.into(),
74 auth: credentials,
75 batch_size: DEFAULT_BATCH_SIZE,
76 insert_id_field: None,
77 write: faucet_core::WriteSpec::default(),
78 }
79 }
80
81 pub fn with_insert_id_field(mut self, field: impl Into<String>) -> Self {
84 self.insert_id_field = Some(field.into());
85 self
86 }
87
88 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
94 self.batch_size = batch_size;
95 self
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn batch_size_defaults_to_default_batch_size() {
105 let config = BigQuerySinkConfig::new(
106 "my-project",
107 "my_dataset",
108 "my_table",
109 BigQueryCredentials::ApplicationDefault,
110 );
111 assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
112 }
113
114 #[test]
115 fn with_batch_size_overrides_default() {
116 let config =
117 BigQuerySinkConfig::new("proj", "ds", "tbl", BigQueryCredentials::ApplicationDefault)
118 .with_batch_size(500);
119 assert_eq!(config.batch_size, 500);
120 }
121
122 #[test]
123 fn config_stores_all_fields() {
124 let config = BigQuerySinkConfig::new(
125 "my-project",
126 "my_dataset",
127 "my_table",
128 BigQueryCredentials::ServiceAccountKeyPath {
129 path: "/path/to/key.json".into(),
130 },
131 );
132 assert_eq!(config.project_id, "my-project");
133 assert_eq!(config.dataset_id, "my_dataset");
134 assert_eq!(config.table_id, "my_table");
135 assert!(matches!(
136 config.auth,
137 BigQueryCredentials::ServiceAccountKeyPath { .. }
138 ));
139 }
140
141 #[test]
142 fn config_with_inline_key() {
143 let config = BigQuerySinkConfig::new(
144 "proj",
145 "ds",
146 "tbl",
147 BigQueryCredentials::ServiceAccountKey {
148 json: r#"{"type":"service_account"}"#.into(),
149 },
150 );
151 if let BigQueryCredentials::ServiceAccountKey { json } = &config.auth {
152 assert!(json.contains("service_account"));
153 } else {
154 panic!("expected ServiceAccountKey");
155 }
156 }
157
158 #[test]
159 fn config_builder_chaining() {
160 let config =
161 BigQuerySinkConfig::new("p", "d", "t", BigQueryCredentials::ApplicationDefault)
162 .with_batch_size(100)
163 .with_batch_size(250);
164 assert_eq!(config.batch_size, 250);
165 }
166
167 #[test]
168 fn config_clone() {
169 let config =
170 BigQuerySinkConfig::new("proj", "ds", "tbl", BigQueryCredentials::ApplicationDefault)
171 .with_batch_size(42);
172 let cloned = config.clone();
173 assert_eq!(cloned.project_id, "proj");
174 assert_eq!(cloned.batch_size, 42);
175 }
176
177 #[test]
178 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
179 let config =
180 BigQuerySinkConfig::new("p", "d", "t", BigQueryCredentials::ApplicationDefault)
181 .with_batch_size(0);
182 assert_eq!(config.batch_size, 0);
183 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
184 }
185
186 #[test]
187 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
188 let config =
189 BigQuerySinkConfig::new("p", "d", "t", BigQueryCredentials::ApplicationDefault)
190 .with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
191 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
192 }
193
194 #[test]
195 fn insert_id_field_defaults_none_and_builder_sets_it() {
196 let config =
197 BigQuerySinkConfig::new("p", "d", "t", BigQueryCredentials::ApplicationDefault);
198 assert!(config.insert_id_field.is_none());
199 let config = config.with_insert_id_field("event_id");
200 assert_eq!(config.insert_id_field.as_deref(), Some("event_id"));
201 }
202
203 #[test]
204 fn insert_id_field_deserializes_from_json() {
205 let json = r#"{
206 "project_id": "p",
207 "dataset_id": "d",
208 "table_id": "t",
209 "auth": {"type": "application_default"},
210 "insert_id_field": "id"
211 }"#;
212 let config: BigQuerySinkConfig = serde_json::from_str(json).unwrap();
213 assert_eq!(config.insert_id_field.as_deref(), Some("id"));
214 }
215
216 #[test]
217 fn batch_size_deserializes_from_json() {
218 let json = r#"{
219 "project_id": "p",
220 "dataset_id": "d",
221 "table_id": "t",
222 "auth": {"type": "application_default"},
223 "batch_size": 250
224 }"#;
225 let config: BigQuerySinkConfig = serde_json::from_str(json).unwrap();
226 assert_eq!(config.batch_size, 250);
227 }
228
229 #[test]
230 fn batch_size_defaults_when_absent_in_json() {
231 let json = r#"{
232 "project_id": "p",
233 "dataset_id": "d",
234 "table_id": "t",
235 "auth": {"type": "application_default"}
236 }"#;
237 let config: BigQuerySinkConfig = serde_json::from_str(json).unwrap();
238 assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
239 }
240
241 #[test]
242 fn write_mode_defaults_to_append() {
243 let config =
244 BigQuerySinkConfig::new("p", "d", "t", BigQueryCredentials::ApplicationDefault);
245 assert_eq!(config.write.write_mode, faucet_core::WriteMode::Append);
246 assert!(config.write.key.is_empty());
247 }
248
249 #[test]
250 fn write_spec_deserializes_flattened() {
251 let json = r#"{
252 "project_id": "p",
253 "dataset_id": "d",
254 "table_id": "t",
255 "auth": {"type": "application_default"},
256 "write_mode": "upsert",
257 "key": ["id"],
258 "delete_marker": {"field": "__op", "values": ["d"]}
259 }"#;
260 let config: BigQuerySinkConfig = serde_json::from_str(json).unwrap();
261 assert_eq!(config.write.write_mode, faucet_core::WriteMode::Upsert);
262 assert_eq!(config.write.key, vec!["id".to_string()]);
263 let dm = config.write.delete_marker.expect("delete_marker");
264 assert_eq!(dm.field, "__op");
265 assert_eq!(dm.values, vec!["d".to_string()]);
266 }
267}