risingwave_connector/
with_options.rs1use std::collections::{BTreeMap, BTreeSet, HashMap};
16use std::marker::PhantomData;
17use std::time::Duration;
18
19use risingwave_pb::id::SecretId;
20use risingwave_pb::secret::PbSecretRef;
21
22use crate::error::ConnectorResult;
23use crate::sink::catalog::SinkFormatDesc;
24use crate::source::cdc::MYSQL_CDC_CONNECTOR;
25use crate::source::cdc::external::ExternalCdcTableType;
26use crate::source::iceberg::ICEBERG_CONNECTOR;
27use crate::source::{
28 ADBC_SNOWFLAKE_CONNECTOR, AZBLOB_CONNECTOR, BATCH_POSIX_FS_CONNECTOR, GCS_CONNECTOR,
29 KAFKA_CONNECTOR, LEGACY_S3_CONNECTOR, OPENDAL_S3_CONNECTOR, POSIX_FS_CONNECTOR,
30 PULSAR_CONNECTOR, UPSTREAM_SOURCE_KEY,
31};
32
33pub trait WithOptions {
40 #[doc(hidden)]
41 #[inline(always)]
42 fn assert_receiver_is_with_options(&self) {}
43}
44
45impl<T: crate::source::cdc::CdcSourceTypeTrait> WithOptions
52 for crate::source::cdc::CdcProperties<T>
53{
54}
55
56impl<T: WithOptions> WithOptions for Option<T> {}
59impl WithOptions for Vec<String> {}
60impl WithOptions for Vec<u64> {}
61impl WithOptions for HashMap<String, String> {}
62impl WithOptions for BTreeMap<String, String> {}
63
64impl WithOptions for String {}
65impl WithOptions for bool {}
66impl WithOptions for usize {}
67impl WithOptions for u8 {}
68impl WithOptions for u16 {}
69impl WithOptions for u32 {}
70impl WithOptions for u64 {}
71impl WithOptions for i32 {}
72impl WithOptions for i64 {}
73impl WithOptions for f64 {}
74impl WithOptions for std::time::Duration {}
75impl WithOptions for crate::connector_common::MqttQualityOfService {}
76impl WithOptions for crate::connector_common::SslMode {}
77impl WithOptions for crate::sink::file_sink::opendal_sink::PathPartitionPrefix {}
78impl WithOptions for crate::sink::kafka::CompressionCodec {}
79impl WithOptions for crate::sink::pulsar::PulsarRoutingMode {}
80impl WithOptions for crate::source::filesystem::file_common::CompressionFormat {}
81impl WithOptions for nexmark::config::RateShape {}
82impl WithOptions for nexmark::event::EventType {}
83impl<T> WithOptions for PhantomData<T> {}
84
85pub trait Get {
86 fn get(&self, key: &str) -> Option<&String>;
87}
88
89pub trait GetKeyIter {
90 fn key_iter(&self) -> impl Iterator<Item = &str>;
91}
92
93impl GetKeyIter for HashMap<String, String> {
94 fn key_iter(&self) -> impl Iterator<Item = &str> {
95 self.keys().map(|s| s.as_str())
96 }
97}
98
99impl Get for HashMap<String, String> {
100 fn get(&self, key: &str) -> Option<&String> {
101 self.get(key)
102 }
103}
104
105impl Get for BTreeMap<String, String> {
106 fn get(&self, key: &str) -> Option<&String> {
107 self.get(key)
108 }
109}
110
111impl GetKeyIter for BTreeMap<String, String> {
112 fn key_iter(&self) -> impl Iterator<Item = &str> {
113 self.keys().map(|s| s.as_str())
114 }
115}
116
117pub trait WithPropertiesExt: Get + GetKeyIter + Sized {
119 #[inline(always)]
120 fn get_connector(&self) -> Option<String> {
121 self.get(UPSTREAM_SOURCE_KEY).map(|s| s.to_lowercase())
122 }
123
124 #[inline(always)]
125 fn is_kafka_connector(&self) -> bool {
126 let Some(connector) = self.get_connector() else {
127 return false;
128 };
129 connector == KAFKA_CONNECTOR
130 }
131
132 #[inline(always)]
133 fn is_pulsar_connector(&self) -> bool {
134 let Some(connector) = self.get_connector() else {
135 return false;
136 };
137 connector == PULSAR_CONNECTOR
138 }
139
140 #[inline(always)]
141 fn is_mysql_cdc_connector(&self) -> bool {
142 let Some(connector) = self.get_connector() else {
143 return false;
144 };
145 connector == MYSQL_CDC_CONNECTOR
146 }
147
148 #[inline(always)]
149 fn get_sync_call_timeout(&self) -> Option<Duration> {
150 const SYNC_CALL_TIMEOUT_KEY: &str = "properties.sync.call.timeout"; self.get(SYNC_CALL_TIMEOUT_KEY)
152 .and_then(|s| duration_str::parse_std(s).ok())
154 }
155
156 #[inline(always)]
157 fn is_cdc_connector(&self) -> bool {
158 let Some(connector) = self.get_connector() else {
159 return false;
160 };
161 connector.contains("-cdc")
162 }
163
164 fn is_shareable_cdc_connector(&self) -> bool {
166 self.is_cdc_connector() && ExternalCdcTableType::from_properties(self).can_backfill()
167 }
168
169 fn enable_transaction_metadata(&self) -> bool {
170 ExternalCdcTableType::from_properties(self).enable_transaction_metadata()
171 }
172
173 fn is_shareable_non_cdc_connector(&self) -> bool {
174 self.is_kafka_connector()
175 }
176
177 #[inline(always)]
178 fn is_iceberg_connector(&self) -> bool {
179 let Some(connector) = self.get_connector() else {
180 return false;
181 };
182 connector == ICEBERG_CONNECTOR
183 }
184
185 fn connector_need_pk(&self) -> bool {
186 !self.is_iceberg_connector()
192 }
193
194 fn is_legacy_fs_connector(&self) -> bool {
195 self.get(UPSTREAM_SOURCE_KEY)
196 .map(|s| s.eq_ignore_ascii_case(LEGACY_S3_CONNECTOR))
197 .unwrap_or(false)
198 }
199
200 fn is_new_fs_connector(&self) -> bool {
201 self.get(UPSTREAM_SOURCE_KEY)
202 .map(|s| {
203 s.eq_ignore_ascii_case(OPENDAL_S3_CONNECTOR)
204 || s.eq_ignore_ascii_case(POSIX_FS_CONNECTOR)
205 || s.eq_ignore_ascii_case(GCS_CONNECTOR)
206 || s.eq_ignore_ascii_case(AZBLOB_CONNECTOR)
207 })
208 .unwrap_or(false)
209 }
210
211 fn is_batch_connector(&self) -> bool {
212 self.get(UPSTREAM_SOURCE_KEY)
213 .map(|s| {
214 s.eq_ignore_ascii_case(BATCH_POSIX_FS_CONNECTOR)
215 || s.eq_ignore_ascii_case(ADBC_SNOWFLAKE_CONNECTOR)
216 })
217 .unwrap_or(false)
218 }
219
220 fn supports_full_reload_refresh(&self) -> bool {
221 self.get(UPSTREAM_SOURCE_KEY)
222 .map(|s| {
223 s.eq_ignore_ascii_case(OPENDAL_S3_CONNECTOR)
224 || s.eq_ignore_ascii_case(GCS_CONNECTOR)
225 || s.eq_ignore_ascii_case(BATCH_POSIX_FS_CONNECTOR)
226 || s.eq_ignore_ascii_case(ICEBERG_CONNECTOR)
227 || s.eq_ignore_ascii_case(ADBC_SNOWFLAKE_CONNECTOR)
228 })
229 .unwrap_or(false)
230 }
231
232 fn requires_singleton(&self) -> bool {
233 self.is_new_fs_connector() || self.is_iceberg_connector() || self.is_batch_connector()
234 }
235}
236
237impl<T: Get + GetKeyIter> WithPropertiesExt for T {}
238
239#[derive(Default, Clone, Debug, PartialEq, Eq, Hash)]
241pub struct WithOptionsSecResolved {
242 inner: BTreeMap<String, String>,
243 secret_ref: BTreeMap<String, PbSecretRef>,
244}
245
246impl std::ops::Deref for WithOptionsSecResolved {
247 type Target = BTreeMap<String, String>;
248
249 fn deref(&self) -> &Self::Target {
250 &self.inner
251 }
252}
253
254impl std::ops::DerefMut for WithOptionsSecResolved {
255 fn deref_mut(&mut self) -> &mut Self::Target {
256 &mut self.inner
257 }
258}
259
260impl WithOptionsSecResolved {
261 pub fn new(inner: BTreeMap<String, String>, secret_ref: BTreeMap<String, PbSecretRef>) -> Self {
263 Self { inner, secret_ref }
264 }
265
266 pub fn as_plaintext(&self) -> &BTreeMap<String, String> {
267 &self.inner
268 }
269
270 pub fn as_secret(&self) -> &BTreeMap<String, PbSecretRef> {
271 &self.secret_ref
272 }
273
274 pub fn handle_update(
275 &mut self,
276 update_alter_props: BTreeMap<String, String>,
277 update_alter_secret_refs: BTreeMap<String, PbSecretRef>,
278 ) -> ConnectorResult<(Vec<SecretId>, Vec<SecretId>)> {
279 let old_secret_ids = self
280 .secret_ref
281 .values()
282 .map(|secret_ref| secret_ref.secret_id)
283 .collect::<BTreeSet<_>>();
284
285 for key in update_alter_props.keys() {
287 if update_alter_secret_refs.contains_key(key) {
288 return Err(
289 anyhow::anyhow!("the key {} is set both in plaintext and secret", key).into(),
290 );
291 }
292 }
293
294 for k in update_alter_props.keys() {
297 self.secret_ref.remove(k);
298 }
299
300 for k in update_alter_secret_refs.keys() {
302 self.inner.remove(k);
304 }
305
306 self.inner.extend(update_alter_props);
307 self.secret_ref.extend(update_alter_secret_refs);
308
309 let new_secret_ids = self
310 .secret_ref
311 .values()
312 .map(|secret_ref| secret_ref.secret_id)
313 .collect::<BTreeSet<_>>();
314 let to_add_secret_dep = new_secret_ids
315 .difference(&old_secret_ids)
316 .copied()
317 .collect();
318 let to_remove_secret_dep = old_secret_ids
319 .difference(&new_secret_ids)
320 .copied()
321 .collect();
322
323 Ok((to_add_secret_dep, to_remove_secret_dep))
324 }
325
326 pub fn without_secrets(inner: BTreeMap<String, String>) -> Self {
328 Self {
329 inner,
330 secret_ref: Default::default(),
331 }
332 }
333
334 pub fn into_parts(self) -> (BTreeMap<String, String>, BTreeMap<String, PbSecretRef>) {
336 (self.inner, self.secret_ref)
337 }
338
339 pub fn value_eq_ignore_case(&self, key: &str, val: &str) -> bool {
340 if let Some(inner_val) = self.inner.get(key)
341 && inner_val.eq_ignore_ascii_case(val)
342 {
343 return true;
344 }
345 false
346 }
347}
348
349impl TryFrom<&WithOptionsSecResolved> for Option<SinkFormatDesc> {
351 type Error = crate::sink::SinkError;
352
353 fn try_from(value: &WithOptionsSecResolved) -> std::result::Result<Self, Self::Error> {
354 let connector = value.get(crate::sink::CONNECTOR_TYPE_KEY);
355 let r#type = value.get(crate::sink::SINK_TYPE_OPTION);
356 match (connector, r#type) {
357 (Some(c), Some(t)) => SinkFormatDesc::from_legacy_type(c, t),
358 _ => Ok(None),
359 }
360 }
361}
362
363impl Get for WithOptionsSecResolved {
364 fn get(&self, key: &str) -> Option<&String> {
365 self.inner.get(key)
366 }
367}
368
369impl GetKeyIter for WithOptionsSecResolved {
370 fn key_iter(&self) -> impl Iterator<Item = &str> {
371 self.inner.keys().map(|s| s.as_str())
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn source_options(connector: &str) -> WithOptionsSecResolved {
380 WithOptionsSecResolved::without_secrets(BTreeMap::from([(
381 UPSTREAM_SOURCE_KEY.to_owned(),
382 connector.to_owned(),
383 )]))
384 }
385
386 #[test]
387 fn test_full_reload_refresh_connector_whitelist() {
388 for connector in [
389 OPENDAL_S3_CONNECTOR,
390 GCS_CONNECTOR,
391 BATCH_POSIX_FS_CONNECTOR,
392 ICEBERG_CONNECTOR,
393 ADBC_SNOWFLAKE_CONNECTOR,
394 ] {
395 assert!(
396 source_options(connector).supports_full_reload_refresh(),
397 "{connector} should support FULL_RELOAD refresh"
398 );
399 }
400
401 for connector in [KAFKA_CONNECTOR, POSIX_FS_CONNECTOR, AZBLOB_CONNECTOR] {
402 assert!(
403 !source_options(connector).supports_full_reload_refresh(),
404 "{connector} should not support FULL_RELOAD refresh"
405 );
406 }
407 }
408}