1use async_trait::async_trait;
6use faucet_core::{FaucetError, RowOutcome, Sink, Source, StreamPage};
7use futures::{Stream, StreamExt};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::pin::Pin;
11use std::sync::Mutex;
12use std::sync::atomic::{AtomicU64, Ordering};
13
14use crate::lifecycle::InferredSchema;
15
16pub struct SampleState {
18 cap: usize,
19 count: AtomicU64,
20 sample: Mutex<Vec<Value>>,
21}
22
23impl SampleState {
24 pub fn new(cap: usize) -> Self {
25 Self {
26 cap,
27 count: AtomicU64::new(0),
28 sample: Mutex::new(Vec::new()),
29 }
30 }
31 pub fn count(&self) -> u64 {
32 self.count.load(Ordering::Relaxed)
33 }
34 fn observe(&self, records: &[Value]) {
35 self.count
36 .fetch_add(records.len() as u64, Ordering::Relaxed);
37 if self.cap == 0 {
38 return;
39 }
40 let mut s = self.sample.lock().unwrap();
41 for r in records {
42 if s.len() >= self.cap {
43 break;
44 }
45 s.push(r.clone());
46 }
47 }
48 pub fn samples(&self) -> Vec<Value> {
53 self.sample.lock().unwrap().clone()
54 }
55
56 pub fn inferred_schema(&self) -> InferredSchema {
58 let sample = self.sample.lock().unwrap();
59 if sample.is_empty() {
60 return InferredSchema::default();
61 }
62 let mut order: Vec<String> = Vec::new();
64 let mut types: HashMap<String, String> = HashMap::new();
65 for rec in sample.iter() {
66 if let Value::Object(map) = rec {
67 for (k, v) in map {
68 if !types.contains_key(k) {
69 order.push(k.clone());
70 }
71 types
72 .entry(k.clone())
73 .or_insert_with(|| ol_type_of(v).to_string());
74 }
75 }
76 }
77 InferredSchema {
78 fields: order
79 .into_iter()
80 .map(|k| {
81 let t = types.remove(&k).unwrap_or_else(|| "string".into());
82 (k, t)
83 })
84 .collect(),
85 }
86 }
87}
88
89fn ol_type_of(v: &Value) -> &'static str {
90 match v {
91 Value::Null => "null",
92 Value::Bool(_) => "boolean",
93 Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
94 Value::Number(_) => "number",
95 Value::String(_) => "string",
96 Value::Array(_) => "array",
97 Value::Object(_) => "object",
98 }
99}
100
101pub struct SamplingSink {
103 inner: Box<dyn Sink>,
104 state: std::sync::Arc<SampleState>,
105}
106
107impl SamplingSink {
108 pub fn new(inner: Box<dyn Sink>, state: std::sync::Arc<SampleState>) -> Self {
109 Self { inner, state }
110 }
111}
112
113#[async_trait]
114impl Sink for SamplingSink {
115 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
116 let n = self.inner.write_batch(records).await?;
117 self.state.observe(records);
118 Ok(n)
119 }
120 async fn write_batch_partial(&self, records: &[Value]) -> Result<Vec<RowOutcome>, FaucetError> {
121 let outcomes = self.inner.write_batch_partial(records).await?;
122 self.state.observe(records);
123 Ok(outcomes)
124 }
125 async fn flush(&self) -> Result<(), FaucetError> {
126 self.inner.flush().await
127 }
128 fn connector_name(&self) -> &'static str {
129 self.inner.connector_name()
130 }
131 fn dataset_uri(&self) -> String {
132 self.inner.dataset_uri()
133 }
134 fn supports_idempotent_writes(&self) -> bool {
139 self.inner.supports_idempotent_writes()
140 }
141 fn sink_guarantee(&self) -> faucet_core::SinkGuarantee {
142 self.inner.sink_guarantee()
143 }
144 fn dedups_by_key(&self) -> bool {
145 self.inner.dedups_by_key()
146 }
147 fn supported_write_modes(&self) -> &'static [faucet_core::WriteMode] {
148 self.inner.supported_write_modes()
149 }
150 async fn write_batch_idempotent(
151 &self,
152 records: &[Value],
153 scope: &str,
154 token: &str,
155 ) -> Result<usize, FaucetError> {
156 let n = self
157 .inner
158 .write_batch_idempotent(records, scope, token)
159 .await?;
160 self.state.observe(records);
161 Ok(n)
162 }
163 async fn last_committed_token(&self, scope: &str) -> Result<Option<String>, FaucetError> {
164 self.inner.last_committed_token(scope).await
165 }
166 async fn current_schema(&self) -> Result<Option<Value>, FaucetError> {
167 self.inner.current_schema().await
168 }
169 fn supports_schema_evolution(&self) -> bool {
170 self.inner.supports_schema_evolution()
171 }
172 async fn evolve_schema(
173 &self,
174 evolution: &faucet_core::SchemaEvolution,
175 ) -> Result<(), FaucetError> {
176 self.inner.evolve_schema(evolution).await
177 }
178 fn is_overwrite(&self) -> bool {
179 self.inner.is_overwrite()
180 }
181 async fn begin_overwrite(&self) -> Result<(), FaucetError> {
182 self.inner.begin_overwrite().await
183 }
184 async fn commit_overwrite(&self) -> Result<(), FaucetError> {
185 self.inner.commit_overwrite().await
186 }
187 async fn abort_overwrite(&self) -> Result<(), FaucetError> {
188 self.inner.abort_overwrite().await
189 }
190}
191
192pub struct SamplingSource {
194 inner: Box<dyn Source>,
195 state: std::sync::Arc<SampleState>,
196}
197
198impl SamplingSource {
199 pub fn new(inner: Box<dyn Source>, state: std::sync::Arc<SampleState>) -> Self {
200 Self { inner, state }
201 }
202}
203
204#[async_trait]
205impl Source for SamplingSource {
206 async fn fetch_with_context(
207 &self,
208 ctx: &HashMap<String, Value>,
209 ) -> Result<Vec<Value>, FaucetError> {
210 self.inner.fetch_with_context(ctx).await
213 }
214 fn stream_pages<'a>(
218 &'a self,
219 ctx: &'a HashMap<String, Value>,
220 batch_size: usize,
221 ) -> Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>> {
222 let state = std::sync::Arc::clone(&self.state);
223 let inner = self.inner.stream_pages(ctx, batch_size);
224 Box::pin(faucet_core::async_stream::try_stream! {
225 let mut inner = inner;
226 while let Some(page) = inner.next().await {
227 let page = page?;
228 state.observe(&page.records);
229 yield page;
230 }
231 })
232 }
233 fn connector_name(&self) -> &'static str {
234 self.inner.connector_name()
235 }
236 fn dataset_uri(&self) -> String {
237 self.inner.dataset_uri()
238 }
239 fn state_key(&self) -> Option<String> {
240 self.inner.state_key()
241 }
242 async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
243 self.inner.apply_start_bookmark(bookmark).await
244 }
245 async fn fetch_with_context_incremental(
251 &self,
252 ctx: &HashMap<String, Value>,
253 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
254 self.inner.fetch_with_context_incremental(ctx).await
255 }
256 fn supports_exactly_once(&self) -> bool {
257 self.inner.supports_exactly_once()
258 }
259 fn replay_guarantee(&self) -> faucet_core::ReplayGuarantee {
260 self.inner.replay_guarantee()
261 }
262 async fn capture_resume_position(&self) -> Result<Option<Value>, FaucetError> {
263 self.inner.capture_resume_position().await
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use async_trait::async_trait;
271 use faucet_core::{FaucetError, Sink};
272 use serde_json::{Value, json};
273 use std::sync::Arc;
274
275 struct CollectSink(std::sync::Mutex<Vec<Value>>);
276 #[async_trait]
277 impl Sink for CollectSink {
278 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
279 self.0.lock().unwrap().extend(records.iter().cloned());
280 Ok(records.len())
281 }
282 fn connector_name(&self) -> &'static str {
283 "collect"
284 }
285 }
286
287 #[tokio::test]
288 async fn sink_counts_and_samples_first_n() {
289 let shared = Arc::new(SampleState::new(2));
290 let inner: Box<dyn Sink> = Box::new(CollectSink(Default::default()));
291 let s = SamplingSink::new(inner, Arc::clone(&shared));
292 s.write_batch(&[json!({"id":1,"name":"a"})]).await.unwrap();
293 s.write_batch(&[json!({"id":2}), json!({"id":3})])
294 .await
295 .unwrap();
296 assert_eq!(shared.count(), 3);
297 let schema = shared.inferred_schema();
299 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
300 assert!(names.contains(&"id"));
301 assert!(names.contains(&"name"));
302 }
303
304 #[tokio::test]
305 async fn sampling_sink_forwards_overwrite_lifecycle() {
306 struct OvwSink(Arc<std::sync::Mutex<Vec<&'static str>>>);
309 #[async_trait]
310 impl Sink for OvwSink {
311 async fn write_batch(&self, r: &[Value]) -> Result<usize, FaucetError> {
312 Ok(r.len())
313 }
314 fn is_overwrite(&self) -> bool {
315 true
316 }
317 async fn begin_overwrite(&self) -> Result<(), FaucetError> {
318 self.0.lock().unwrap().push("begin");
319 Ok(())
320 }
321 async fn commit_overwrite(&self) -> Result<(), FaucetError> {
322 self.0.lock().unwrap().push("commit");
323 Ok(())
324 }
325 async fn abort_overwrite(&self) -> Result<(), FaucetError> {
326 self.0.lock().unwrap().push("abort");
327 Ok(())
328 }
329 }
330 let log = Arc::new(std::sync::Mutex::new(Vec::new()));
331 let inner: Box<dyn Sink> = Box::new(OvwSink(Arc::clone(&log)));
332 let s = SamplingSink::new(inner, Arc::new(SampleState::new(2)));
333 assert!(s.is_overwrite());
334 assert_eq!(s.write_batch(&[json!({"id": 1})]).await.unwrap(), 1);
336 s.begin_overwrite().await.unwrap();
337 s.commit_overwrite().await.unwrap();
338 s.abort_overwrite().await.unwrap();
339 assert_eq!(*log.lock().unwrap(), vec!["begin", "commit", "abort"]);
340 }
341
342 struct TwoRowSource;
343 #[async_trait]
344 impl faucet_core::Source for TwoRowSource {
345 async fn fetch_with_context(
346 &self,
347 _: &std::collections::HashMap<String, Value>,
348 ) -> Result<Vec<Value>, FaucetError> {
349 Ok(vec![json!({"id": 1}), json!({"id": 2})])
350 }
351 fn connector_name(&self) -> &'static str {
352 "tworow"
353 }
354 }
355
356 #[tokio::test]
357 async fn source_samples_streamed_pages_without_buffering_override() {
358 use faucet_core::Source as _;
359 use futures::StreamExt as _;
360 let shared = Arc::new(SampleState::new(10));
361 let s = SamplingSource::new(Box::new(TwoRowSource), Arc::clone(&shared));
362 let ctx = std::collections::HashMap::new();
363 let mut pages = s.stream_pages(&ctx, 1000);
364 while let Some(p) = pages.next().await {
365 let _ = p.unwrap();
366 }
367 assert_eq!(shared.count(), 2);
368 let schema = shared.inferred_schema();
369 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
370 assert!(names.contains(&"id"));
371 }
372
373 struct IdemSink;
378 #[async_trait]
379 impl Sink for IdemSink {
380 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
381 Ok(records.len())
382 }
383 fn connector_name(&self) -> &'static str {
384 "idem"
385 }
386 fn supports_idempotent_writes(&self) -> bool {
387 true
388 }
389 fn dedups_by_key(&self) -> bool {
390 true
391 }
392 fn supported_write_modes(&self) -> &'static [faucet_core::WriteMode] {
393 &[
394 faucet_core::WriteMode::Append,
395 faucet_core::WriteMode::Upsert,
396 ]
397 }
398 async fn write_batch_idempotent(
399 &self,
400 records: &[Value],
401 _scope: &str,
402 _token: &str,
403 ) -> Result<usize, FaucetError> {
404 Ok(records.len())
405 }
406 async fn last_committed_token(&self, _scope: &str) -> Result<Option<String>, FaucetError> {
407 Ok(Some("tok".into()))
408 }
409 fn supports_schema_evolution(&self) -> bool {
410 true
411 }
412 async fn current_schema(&self) -> Result<Option<Value>, FaucetError> {
413 Ok(Some(json!({"type": "object", "properties": {}})))
414 }
415 }
416
417 #[tokio::test]
418 async fn sink_forwards_capabilities_and_samples_idempotent_writes() {
419 let shared = Arc::new(SampleState::new(10));
420 let s = SamplingSink::new(Box::new(IdemSink), Arc::clone(&shared));
421 assert!(s.supports_idempotent_writes());
422 assert!(s.dedups_by_key());
423 assert_eq!(
424 s.sink_guarantee(),
425 faucet_core::SinkGuarantee::AtomicWatermark
426 );
427 assert!(
428 s.supported_write_modes()
429 .contains(&faucet_core::WriteMode::Upsert)
430 );
431 assert!(s.supports_schema_evolution());
432 assert!(s.current_schema().await.unwrap().is_some());
433 assert_eq!(
434 s.last_committed_token("k").await.unwrap(),
435 Some("tok".into())
436 );
437 s.write_batch_idempotent(&[json!({"id": 1})], "k", "t")
439 .await
440 .unwrap();
441 assert_eq!(shared.count(), 1);
442 }
443
444 struct BookmarkedSource;
445 #[async_trait]
446 impl faucet_core::Source for BookmarkedSource {
447 async fn fetch_with_context(
448 &self,
449 _: &std::collections::HashMap<String, Value>,
450 ) -> Result<Vec<Value>, FaucetError> {
451 Ok(vec![json!({"id": 1})])
452 }
453 async fn fetch_with_context_incremental(
454 &self,
455 _: &std::collections::HashMap<String, Value>,
456 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
457 Ok((vec![json!({"id": 1})], Some(json!("bm"))))
458 }
459 fn supports_exactly_once(&self) -> bool {
460 true
461 }
462 async fn capture_resume_position(&self) -> Result<Option<Value>, FaucetError> {
463 Ok(Some(json!("pos")))
464 }
465 fn connector_name(&self) -> &'static str {
466 "bookmarked"
467 }
468 }
469
470 #[tokio::test]
471 async fn source_forwards_bookmarks_and_capabilities() {
472 use faucet_core::Source as _;
473 let shared = Arc::new(SampleState::new(10));
474 let s = SamplingSource::new(Box::new(BookmarkedSource), Arc::clone(&shared));
475 let (_, bm) = s
479 .fetch_with_context_incremental(&std::collections::HashMap::new())
480 .await
481 .unwrap();
482 assert_eq!(bm, Some(json!("bm")));
483 assert!(s.supports_exactly_once());
484 assert_eq!(
485 s.replay_guarantee(),
486 faucet_core::ReplayGuarantee::Deterministic
487 );
488 assert_eq!(
489 s.capture_resume_position().await.unwrap(),
490 Some(json!("pos"))
491 );
492 }
493}