1use crate::idempotency::{ReplayGuarantee, SinkGuarantee};
33use crate::traits::{Sink, Source};
34use crate::write_mode::{WriteMode, WriteSpec};
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42#[non_exhaustive]
43pub enum SchemaReport {
44 #[default]
46 Undeclared,
47 Reported,
50 Schemaless,
52 DropsUnknownFields,
56}
57
58impl SchemaReport {
59 pub fn as_str(&self) -> &'static str {
61 match self {
62 SchemaReport::Undeclared => "undeclared",
63 SchemaReport::Reported => "reported",
64 SchemaReport::Schemaless => "schemaless",
65 SchemaReport::DropsUnknownFields => "drops_unknown_fields",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
77#[non_exhaustive]
78pub struct Capabilities {
79 write_modes: Vec<WriteMode>,
80 idempotent_writes: bool,
81 keyed_upsert: bool,
82 keyed_dedup: bool,
83 cleanup: bool,
84 rollback: bool,
85 schema_evolution: bool,
86 schema_report: SchemaReport,
87 scoped_overwrite: bool,
88 direct_overwrite: bool,
89 staged_load: bool,
90 exactly_once: bool,
91 discover: bool,
92 shardable: bool,
93 range_read: bool,
94 range_digest: bool,
95 reports_lag: bool,
96 columnar: bool,
97 native: bool,
98 table_routing: bool,
99 read_side_effects: bool,
100}
101
102const MODE_ORDER: [WriteMode; 4] = [
103 WriteMode::Append,
104 WriteMode::Upsert,
105 WriteMode::Delete,
106 WriteMode::Overwrite,
107];
108
109fn mode_rank(m: &WriteMode) -> usize {
110 MODE_ORDER
111 .iter()
112 .position(|o| o == m)
113 .unwrap_or(MODE_ORDER.len())
114}
115
116impl Capabilities {
117 pub fn builder() -> CapabilitiesBuilder {
119 CapabilitiesBuilder(Capabilities::default())
120 }
121
122 pub fn to_builder(&self) -> CapabilitiesBuilder {
124 CapabilitiesBuilder(self.clone())
125 }
126
127 pub fn plain_source() -> Self {
129 Self::default()
130 }
131
132 pub fn append_only_sink() -> Self {
134 Self::builder().write_modes([WriteMode::Append]).build()
135 }
136
137 pub fn of_source<S: Source + ?Sized>(source: &S) -> Self {
142 #[cfg(feature = "arrow")]
143 let columnar = source.supports_columnar();
144 #[cfg(not(feature = "arrow"))]
145 let columnar = false;
146 Self::builder()
147 .exactly_once(source.replay_guarantee() == ReplayGuarantee::Deterministic)
148 .discover(source.supports_discover())
149 .shardable(source.is_shardable())
150 .columnar(columnar)
151 .native(!source.native_output_formats().is_empty())
152 .build()
153 }
154
155 pub fn of_sink<S: Sink + ?Sized>(sink: &S) -> Self {
158 #[cfg(feature = "arrow")]
159 let columnar = sink.supports_columnar();
160 #[cfg(not(feature = "arrow"))]
161 let columnar = false;
162 let modes = sink.supported_write_modes();
163 let schema_evolution = sink.supports_schema_evolution();
164 Self::builder()
165 .write_modes(modes.iter().copied())
166 .idempotent_writes(sink.supports_idempotent_writes())
167 .keyed_upsert(
168 modes.contains(&WriteMode::Upsert)
169 && sink.sink_guarantee() != SinkGuarantee::AtLeastOnce,
170 )
171 .keyed_dedup(sink.dedups_by_key())
172 .cleanup(sink.supports_cleanup())
173 .rollback(sink.supports_rollback())
174 .schema_evolution(schema_evolution)
175 .schema_report(if schema_evolution {
176 SchemaReport::Reported
177 } else {
178 SchemaReport::Undeclared
179 })
180 .staged_load(sink.supports_staged_load())
181 .columnar(columnar)
182 .native(!sink.native_load_capabilities().is_empty())
183 .build()
184 }
185
186 pub fn decorated_source<S: Source + ?Sized>(outer: &S, inner: &Capabilities) -> Self {
190 Self::of_source(outer)
191 .to_builder()
192 .range_read(inner.range_read)
193 .range_digest(inner.range_digest)
194 .reports_lag(inner.reports_lag)
195 .table_routing(inner.table_routing)
196 .read_side_effects(inner.read_side_effects)
197 .build()
198 }
199
200 pub fn decorated_sink<S: Sink + ?Sized>(outer: &S, inner: &Capabilities) -> Self {
202 Self::of_sink(outer)
203 .to_builder()
204 .schema_report(inner.schema_report)
205 .scoped_overwrite(inner.scoped_overwrite)
206 .direct_overwrite(inner.direct_overwrite)
207 .build()
208 }
209
210 pub fn write_modes(&self) -> &[WriteMode] {
212 &self.write_modes
213 }
214
215 pub fn supports_write_mode(&self, mode: WriteMode) -> bool {
217 self.write_modes.contains(&mode)
218 }
219
220 pub fn overwrite(&self) -> bool {
222 self.supports_write_mode(WriteMode::Overwrite)
223 }
224
225 pub fn idempotent_writes(&self) -> bool {
227 self.idempotent_writes
228 }
229
230 pub fn keyed_upsert(&self) -> bool {
234 self.keyed_upsert
235 }
236
237 pub fn keyed_dedup(&self) -> bool {
239 self.keyed_dedup
240 }
241
242 pub fn cleanup(&self) -> bool {
244 self.cleanup
245 }
246
247 pub fn rollback(&self) -> bool {
249 self.rollback
250 }
251
252 pub fn schema_evolution(&self) -> bool {
254 self.schema_evolution
255 }
256
257 pub fn schema_report(&self) -> SchemaReport {
259 self.schema_report
260 }
261
262 pub fn scoped_overwrite(&self) -> bool {
264 self.scoped_overwrite
265 }
266
267 pub fn direct_overwrite(&self) -> bool {
269 self.direct_overwrite
270 }
271
272 pub fn staged_load(&self) -> bool {
274 self.staged_load
275 }
276
277 pub fn exactly_once(&self) -> bool {
279 self.exactly_once
280 }
281
282 pub fn discover(&self) -> bool {
284 self.discover
285 }
286
287 pub fn shardable(&self) -> bool {
289 self.shardable
290 }
291
292 pub fn range_read(&self) -> bool {
295 self.range_read
296 }
297
298 pub fn range_digest(&self) -> bool {
300 self.range_digest
301 }
302
303 pub fn reports_lag(&self) -> bool {
305 self.reports_lag
306 }
307
308 pub fn columnar(&self) -> bool {
310 self.columnar
311 }
312
313 pub fn native(&self) -> bool {
315 self.native
316 }
317
318 pub fn table_routing(&self) -> bool {
321 self.table_routing
322 }
323
324 pub fn read_side_effects(&self) -> bool {
328 self.read_side_effects
329 }
330
331 pub fn replay_guarantee(&self) -> ReplayGuarantee {
333 if self.exactly_once {
334 ReplayGuarantee::Deterministic
335 } else {
336 ReplayGuarantee::NonDeterministic
337 }
338 }
339
340 pub fn sink_guarantee(&self) -> SinkGuarantee {
342 if self.idempotent_writes {
343 SinkGuarantee::AtomicWatermark
344 } else if self.keyed_upsert {
345 SinkGuarantee::KeyedUpsert
346 } else {
347 SinkGuarantee::AtLeastOnce
348 }
349 }
350}
351
352#[derive(Debug, Clone, Default)]
354pub struct CapabilitiesBuilder(Capabilities);
355
356macro_rules! flag {
357 ($($name:ident),* $(,)?) => {
358 $(
359 #[doc = concat!("Set `", stringify!($name), "`.")]
360 #[must_use]
361 pub fn $name(mut self, on: bool) -> Self {
362 self.0.$name = on;
363 self
364 }
365 )*
366 };
367}
368
369impl CapabilitiesBuilder {
370 #[must_use]
372 pub fn write_modes(mut self, modes: impl IntoIterator<Item = WriteMode>) -> Self {
373 let mut v: Vec<WriteMode> = Vec::new();
374 for m in modes {
375 if !v.contains(&m) {
376 v.push(m);
377 }
378 }
379 v.sort_by_key(mode_rank);
380 self.0.write_modes = v;
381 self
382 }
383
384 #[must_use]
386 pub fn schema_report(mut self, report: SchemaReport) -> Self {
387 self.0.schema_report = report;
388 self
389 }
390
391 flag!(
392 idempotent_writes,
393 keyed_upsert,
394 keyed_dedup,
395 cleanup,
396 rollback,
397 schema_evolution,
398 scoped_overwrite,
399 direct_overwrite,
400 staged_load,
401 exactly_once,
402 discover,
403 shardable,
404 range_read,
405 range_digest,
406 reports_lag,
407 columnar,
408 native,
409 table_routing,
410 read_side_effects,
411 );
412
413 pub fn build(self) -> Capabilities {
415 self.0
416 }
417}
418
419#[derive(Clone, Copy)]
423#[non_exhaustive]
424pub struct ConnectorDescriptor {
425 kind: &'static str,
426 capabilities: fn(&Value) -> Capabilities,
427}
428
429impl ConnectorDescriptor {
430 pub const fn new(kind: &'static str, capabilities: fn(&Value) -> Capabilities) -> Self {
433 Self { kind, capabilities }
434 }
435
436 pub fn kind(&self) -> &'static str {
438 self.kind
439 }
440
441 pub fn capabilities(&self, config: &Value) -> Capabilities {
443 (self.capabilities)(config)
444 }
445
446 pub fn kind_capabilities(&self) -> Capabilities {
450 (self.capabilities)(&Value::Null)
451 }
452}
453
454impl std::fmt::Debug for ConnectorDescriptor {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 f.debug_struct("ConnectorDescriptor")
457 .field("kind", &self.kind)
458 .finish_non_exhaustive()
459 }
460}
461
462pub fn plain_source(_config: &Value) -> Capabilities {
464 Capabilities::plain_source()
465}
466
467pub fn append_only_sink(_config: &Value) -> Capabilities {
469 Capabilities::append_only_sink()
470}
471
472pub mod config {
474 use super::*;
475
476 pub fn write_spec(config: &Value) -> WriteSpec {
479 let mut spec = WriteSpec::default();
480 if let Some(mode) = config.get("write_mode")
481 && let Ok(m) = serde_json::from_value::<WriteMode>(mode.clone())
482 {
483 spec.write_mode = m;
484 }
485 if let Some(Value::Array(keys)) = config.get("key") {
486 spec.key = keys
487 .iter()
488 .filter_map(|k| k.as_str().map(str::to_string))
489 .collect();
490 }
491 spec
492 }
493
494 pub fn dedups_by_key(config: &Value) -> bool {
496 write_spec(config).dedups_by_key()
497 }
498
499 pub fn str_field<'a>(config: &'a Value, field: &str) -> Option<&'a str> {
501 config.get(field).and_then(Value::as_str)
502 }
503
504 pub fn bool_field(config: &Value, field: &str, default: bool) -> bool {
506 config
507 .get(field)
508 .and_then(Value::as_bool)
509 .unwrap_or(default)
510 }
511
512 pub fn is_set(config: &Value, field: &str) -> bool {
514 config.get(field).is_some_and(|v| !v.is_null())
515 }
516
517 pub fn tag<'a>(config: &'a Value, field: &str) -> Option<&'a str> {
520 match config.get(field)? {
521 Value::String(s) => Some(s),
522 Value::Object(o) => o
523 .get("type")
524 .and_then(Value::as_str)
525 .or_else(|| (o.len() == 1).then(|| o.keys().next().map(String::as_str))?),
526 _ => None,
527 }
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::FaucetError;
535 use async_trait::async_trait;
536 use serde_json::json;
537
538 struct Plain;
539 #[async_trait]
540 impl Source for Plain {
541 async fn fetch_with_context(
542 &self,
543 _: &std::collections::HashMap<String, Value>,
544 ) -> Result<Vec<Value>, FaucetError> {
545 Ok(vec![])
546 }
547 }
548 #[async_trait]
549 impl Sink for Plain {
550 async fn write_batch(&self, r: &[Value]) -> Result<usize, FaucetError> {
551 Ok(r.len())
552 }
553 }
554
555 struct Rich;
556 #[async_trait]
557 impl Source for Rich {
558 async fn fetch_with_context(
559 &self,
560 _: &std::collections::HashMap<String, Value>,
561 ) -> Result<Vec<Value>, FaucetError> {
562 Ok(vec![])
563 }
564 fn supports_exactly_once(&self) -> bool {
565 true
566 }
567 fn supports_discover(&self) -> bool {
568 true
569 }
570 fn is_shardable(&self) -> bool {
571 true
572 }
573 fn native_output_formats(&self) -> &'static [crate::native::NativeFormat] {
574 &[crate::native::NativeFormat::NdJson]
575 }
576 }
577 #[async_trait]
578 impl Sink for Rich {
579 async fn write_batch(&self, r: &[Value]) -> Result<usize, FaucetError> {
580 Ok(r.len())
581 }
582 fn supported_write_modes(&self) -> &'static [WriteMode] {
583 &[WriteMode::Overwrite, WriteMode::Upsert, WriteMode::Append]
584 }
585 fn supports_idempotent_writes(&self) -> bool {
586 true
587 }
588 fn dedups_by_key(&self) -> bool {
589 true
590 }
591 fn supports_cleanup(&self) -> bool {
592 true
593 }
594 fn supports_rollback(&self) -> bool {
595 true
596 }
597 fn supports_schema_evolution(&self) -> bool {
598 true
599 }
600 fn supports_staged_load(&self) -> bool {
601 true
602 }
603 }
604
605 #[tokio::test]
608 async fn descriptors_built_at_run_time_and_the_test_connectors() {
609 let d = ConnectorDescriptor::new("rich", |_| Sink::capabilities(&Rich));
610 assert_eq!(d.kind(), "rich");
611 assert!(d.kind_capabilities().idempotent_writes());
612 let rows = [json!({"a": 1})];
613 for sink in [&Plain as &dyn Sink, &Rich] {
614 assert_eq!(sink.write_batch(&rows).await.unwrap(), 1);
615 }
616 for source in [&Plain as &dyn Source, &Rich] {
617 assert!(source.fetch_all().await.unwrap().is_empty());
618 }
619 }
620
621 #[test]
622 fn plain_connectors_derive_nothing() {
623 assert_eq!(Source::capabilities(&Plain), Capabilities::plain_source());
624 assert_eq!(Sink::capabilities(&Plain), Capabilities::append_only_sink());
625 assert_eq!(plain_source(&Value::Null), Capabilities::plain_source());
626 assert_eq!(
627 append_only_sink(&Value::Null),
628 Capabilities::append_only_sink()
629 );
630 let s = Capabilities::append_only_sink();
631 assert_eq!(s.sink_guarantee(), SinkGuarantee::AtLeastOnce);
632 assert_eq!(s.replay_guarantee(), ReplayGuarantee::NonDeterministic);
633 assert!(!s.overwrite());
634 assert_eq!(s.schema_report(), SchemaReport::Undeclared);
635 }
636
637 #[test]
638 fn derivation_reads_every_trait_method() {
639 let src = Source::capabilities(&Rich);
640 assert!(src.exactly_once() && src.discover() && src.shardable() && src.native());
641 assert!(!src.columnar() && !src.range_read() && !src.range_digest());
642 assert!(!src.reports_lag());
643 assert_eq!(src.replay_guarantee(), ReplayGuarantee::Deterministic);
644
645 let sink = Sink::capabilities(&Rich);
646 assert_eq!(
647 sink.write_modes(),
648 &[WriteMode::Append, WriteMode::Upsert, WriteMode::Overwrite]
649 );
650 assert!(sink.idempotent_writes() && sink.keyed_upsert() && sink.keyed_dedup());
651 assert!(sink.cleanup() && sink.rollback() && sink.schema_evolution());
652 assert!(sink.staged_load() && sink.overwrite());
653 assert!(!sink.scoped_overwrite() && !sink.direct_overwrite() && !sink.native());
654 assert_eq!(sink.schema_report(), SchemaReport::Reported);
655 assert_eq!(sink.sink_guarantee(), SinkGuarantee::AtomicWatermark);
656 }
657
658 #[test]
659 fn decorated_keeps_inner_declarations_and_outer_derivations() {
660 let inner = Capabilities::builder()
661 .range_read(true)
662 .range_digest(true)
663 .reports_lag(true)
664 .table_routing(true)
665 .read_side_effects(true)
666 .schema_report(SchemaReport::DropsUnknownFields)
667 .scoped_overwrite(true)
668 .direct_overwrite(true)
669 .discover(true)
670 .build();
671 let src = Capabilities::decorated_source(&Plain, &inner);
672 assert!(src.range_read() && src.range_digest() && src.reports_lag());
673 assert!(src.table_routing() && src.read_side_effects());
674 assert!(!src.discover(), "derived from the outer connector");
675 let sink = Capabilities::decorated_sink(&Plain, &inner);
676 assert_eq!(sink.schema_report(), SchemaReport::DropsUnknownFields);
677 assert!(sink.scoped_overwrite() && sink.direct_overwrite());
678 assert_eq!(sink.write_modes(), &[WriteMode::Append]);
679 }
680
681 #[test]
682 fn builder_sets_every_flag_and_round_trips() {
683 let c = Capabilities::builder()
684 .write_modes([WriteMode::Delete, WriteMode::Append, WriteMode::Delete])
685 .keyed_upsert(true)
686 .scoped_overwrite(true)
687 .direct_overwrite(true)
688 .range_read(true)
689 .range_digest(true)
690 .reports_lag(true)
691 .columnar(true)
692 .schema_report(SchemaReport::DropsUnknownFields)
693 .build();
694 assert_eq!(c.write_modes(), &[WriteMode::Append, WriteMode::Delete]);
695 assert!(c.supports_write_mode(WriteMode::Delete));
696 assert!(c.scoped_overwrite() && c.direct_overwrite());
697 assert!(c.range_read() && c.range_digest() && c.reports_lag() && c.columnar());
698 assert_eq!(c.sink_guarantee(), SinkGuarantee::KeyedUpsert);
699 assert_eq!(c.to_builder().build(), c);
700 let off = c.to_builder().columnar(false).build();
701 assert!(!off.columnar());
702 assert!(!c.table_routing() && !c.read_side_effects());
703 let cdc = c
704 .to_builder()
705 .table_routing(true)
706 .read_side_effects(true)
707 .build();
708 assert!(cdc.table_routing() && cdc.read_side_effects());
709 let v = serde_json::to_value(&c).unwrap();
710 assert_eq!(v["schema_report"], "drops_unknown_fields");
711 assert_eq!(v["write_modes"], json!(["append", "delete"]));
712 }
713
714 #[test]
715 fn schema_report_names() {
716 for (r, n) in [
717 (SchemaReport::Undeclared, "undeclared"),
718 (SchemaReport::Reported, "reported"),
719 (SchemaReport::Schemaless, "schemaless"),
720 (SchemaReport::DropsUnknownFields, "drops_unknown_fields"),
721 ] {
722 assert_eq!(r.as_str(), n);
723 assert_eq!(serde_json::to_value(r).unwrap(), json!(n));
724 }
725 }
726
727 fn rich(c: &Value) -> Capabilities {
728 Capabilities::append_only_sink()
729 .to_builder()
730 .keyed_dedup(config::dedups_by_key(c))
731 .build()
732 }
733
734 #[test]
735 fn descriptor_answers_from_config() {
736 const D: ConnectorDescriptor = ConnectorDescriptor::new("rich", rich);
737 assert_eq!(D.kind(), "rich");
738 assert_eq!(D.kind_capabilities(), Capabilities::append_only_sink());
739 assert!(
740 D.capabilities(&json!({"write_mode": "upsert", "key": ["id"]}))
741 .keyed_dedup()
742 );
743 assert!(
744 !D.capabilities(&json!({"write_mode": "upsert"}))
745 .keyed_dedup()
746 );
747 assert!(!D.capabilities(&json!({"write_mode": "${x}"})).keyed_dedup());
748 assert!(format!("{D:?}").contains("rich"));
749 }
750
751 #[test]
752 fn config_helpers_are_lenient() {
753 let c = json!({
754 "write_mode": "delete", "key": ["a", 1], "s": "x", "b": true,
755 "n": null, "m": {"auto_map": {}}, "t": {"type": "jsonb"}, "e": "plain"
756 });
757 let spec = config::write_spec(&c);
758 assert_eq!(spec.write_mode, WriteMode::Delete);
759 assert_eq!(spec.key, vec!["a".to_string()]);
760 assert_eq!(config::str_field(&c, "s"), Some("x"));
761 assert!(config::bool_field(&c, "b", false));
762 assert!(config::bool_field(&c, "missing", true));
763 assert!(!config::is_set(&c, "n") && config::is_set(&c, "s"));
764 assert_eq!(config::tag(&c, "m"), Some("auto_map"));
765 assert_eq!(config::tag(&c, "t"), Some("jsonb"));
766 assert_eq!(config::tag(&c, "e"), Some("plain"));
767 assert_eq!(config::tag(&c, "b"), None);
768 assert_eq!(config::tag(&c, "missing"), None);
769 assert_eq!(
770 config::write_spec(&Value::Null).write_mode,
771 WriteMode::Append
772 );
773 }
774}