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}
179
180pub struct SamplingSource {
182 inner: Box<dyn Source>,
183 state: std::sync::Arc<SampleState>,
184}
185
186impl SamplingSource {
187 pub fn new(inner: Box<dyn Source>, state: std::sync::Arc<SampleState>) -> Self {
188 Self { inner, state }
189 }
190}
191
192#[async_trait]
193impl Source for SamplingSource {
194 async fn fetch_with_context(
195 &self,
196 ctx: &HashMap<String, Value>,
197 ) -> Result<Vec<Value>, FaucetError> {
198 self.inner.fetch_with_context(ctx).await
201 }
202 fn stream_pages<'a>(
206 &'a self,
207 ctx: &'a HashMap<String, Value>,
208 batch_size: usize,
209 ) -> Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>> {
210 let state = std::sync::Arc::clone(&self.state);
211 let inner = self.inner.stream_pages(ctx, batch_size);
212 Box::pin(faucet_core::async_stream::try_stream! {
213 let mut inner = inner;
214 while let Some(page) = inner.next().await {
215 let page = page?;
216 state.observe(&page.records);
217 yield page;
218 }
219 })
220 }
221 fn connector_name(&self) -> &'static str {
222 self.inner.connector_name()
223 }
224 fn dataset_uri(&self) -> String {
225 self.inner.dataset_uri()
226 }
227 fn state_key(&self) -> Option<String> {
228 self.inner.state_key()
229 }
230 async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
231 self.inner.apply_start_bookmark(bookmark).await
232 }
233 async fn fetch_with_context_incremental(
239 &self,
240 ctx: &HashMap<String, Value>,
241 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
242 self.inner.fetch_with_context_incremental(ctx).await
243 }
244 fn supports_exactly_once(&self) -> bool {
245 self.inner.supports_exactly_once()
246 }
247 fn replay_guarantee(&self) -> faucet_core::ReplayGuarantee {
248 self.inner.replay_guarantee()
249 }
250 async fn capture_resume_position(&self) -> Result<Option<Value>, FaucetError> {
251 self.inner.capture_resume_position().await
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use async_trait::async_trait;
259 use faucet_core::{FaucetError, Sink};
260 use serde_json::{Value, json};
261 use std::sync::Arc;
262
263 struct CollectSink(std::sync::Mutex<Vec<Value>>);
264 #[async_trait]
265 impl Sink for CollectSink {
266 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
267 self.0.lock().unwrap().extend(records.iter().cloned());
268 Ok(records.len())
269 }
270 fn connector_name(&self) -> &'static str {
271 "collect"
272 }
273 }
274
275 #[tokio::test]
276 async fn sink_counts_and_samples_first_n() {
277 let shared = Arc::new(SampleState::new(2));
278 let inner: Box<dyn Sink> = Box::new(CollectSink(Default::default()));
279 let s = SamplingSink::new(inner, Arc::clone(&shared));
280 s.write_batch(&[json!({"id":1,"name":"a"})]).await.unwrap();
281 s.write_batch(&[json!({"id":2}), json!({"id":3})])
282 .await
283 .unwrap();
284 assert_eq!(shared.count(), 3);
285 let schema = shared.inferred_schema();
287 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
288 assert!(names.contains(&"id"));
289 assert!(names.contains(&"name"));
290 }
291
292 struct TwoRowSource;
293 #[async_trait]
294 impl faucet_core::Source for TwoRowSource {
295 async fn fetch_with_context(
296 &self,
297 _: &std::collections::HashMap<String, Value>,
298 ) -> Result<Vec<Value>, FaucetError> {
299 Ok(vec![json!({"id": 1}), json!({"id": 2})])
300 }
301 fn connector_name(&self) -> &'static str {
302 "tworow"
303 }
304 }
305
306 #[tokio::test]
307 async fn source_samples_streamed_pages_without_buffering_override() {
308 use faucet_core::Source as _;
309 use futures::StreamExt as _;
310 let shared = Arc::new(SampleState::new(10));
311 let s = SamplingSource::new(Box::new(TwoRowSource), Arc::clone(&shared));
312 let ctx = std::collections::HashMap::new();
313 let mut pages = s.stream_pages(&ctx, 1000);
314 while let Some(p) = pages.next().await {
315 let _ = p.unwrap();
316 }
317 assert_eq!(shared.count(), 2);
318 let schema = shared.inferred_schema();
319 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
320 assert!(names.contains(&"id"));
321 }
322
323 struct IdemSink;
328 #[async_trait]
329 impl Sink for IdemSink {
330 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
331 Ok(records.len())
332 }
333 fn connector_name(&self) -> &'static str {
334 "idem"
335 }
336 fn supports_idempotent_writes(&self) -> bool {
337 true
338 }
339 fn dedups_by_key(&self) -> bool {
340 true
341 }
342 fn supported_write_modes(&self) -> &'static [faucet_core::WriteMode] {
343 &[
344 faucet_core::WriteMode::Append,
345 faucet_core::WriteMode::Upsert,
346 ]
347 }
348 async fn write_batch_idempotent(
349 &self,
350 records: &[Value],
351 _scope: &str,
352 _token: &str,
353 ) -> Result<usize, FaucetError> {
354 Ok(records.len())
355 }
356 async fn last_committed_token(&self, _scope: &str) -> Result<Option<String>, FaucetError> {
357 Ok(Some("tok".into()))
358 }
359 fn supports_schema_evolution(&self) -> bool {
360 true
361 }
362 async fn current_schema(&self) -> Result<Option<Value>, FaucetError> {
363 Ok(Some(json!({"type": "object", "properties": {}})))
364 }
365 }
366
367 #[tokio::test]
368 async fn sink_forwards_capabilities_and_samples_idempotent_writes() {
369 let shared = Arc::new(SampleState::new(10));
370 let s = SamplingSink::new(Box::new(IdemSink), Arc::clone(&shared));
371 assert!(s.supports_idempotent_writes());
372 assert!(s.dedups_by_key());
373 assert_eq!(
374 s.sink_guarantee(),
375 faucet_core::SinkGuarantee::AtomicWatermark
376 );
377 assert!(
378 s.supported_write_modes()
379 .contains(&faucet_core::WriteMode::Upsert)
380 );
381 assert!(s.supports_schema_evolution());
382 assert!(s.current_schema().await.unwrap().is_some());
383 assert_eq!(
384 s.last_committed_token("k").await.unwrap(),
385 Some("tok".into())
386 );
387 s.write_batch_idempotent(&[json!({"id": 1})], "k", "t")
389 .await
390 .unwrap();
391 assert_eq!(shared.count(), 1);
392 }
393
394 struct BookmarkedSource;
395 #[async_trait]
396 impl faucet_core::Source for BookmarkedSource {
397 async fn fetch_with_context(
398 &self,
399 _: &std::collections::HashMap<String, Value>,
400 ) -> Result<Vec<Value>, FaucetError> {
401 Ok(vec![json!({"id": 1})])
402 }
403 async fn fetch_with_context_incremental(
404 &self,
405 _: &std::collections::HashMap<String, Value>,
406 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
407 Ok((vec![json!({"id": 1})], Some(json!("bm"))))
408 }
409 fn supports_exactly_once(&self) -> bool {
410 true
411 }
412 async fn capture_resume_position(&self) -> Result<Option<Value>, FaucetError> {
413 Ok(Some(json!("pos")))
414 }
415 fn connector_name(&self) -> &'static str {
416 "bookmarked"
417 }
418 }
419
420 #[tokio::test]
421 async fn source_forwards_bookmarks_and_capabilities() {
422 use faucet_core::Source as _;
423 let shared = Arc::new(SampleState::new(10));
424 let s = SamplingSource::new(Box::new(BookmarkedSource), Arc::clone(&shared));
425 let (_, bm) = s
429 .fetch_with_context_incremental(&std::collections::HashMap::new())
430 .await
431 .unwrap();
432 assert_eq!(bm, Some(json!("bm")));
433 assert!(s.supports_exactly_once());
434 assert_eq!(
435 s.replay_guarantee(),
436 faucet_core::ReplayGuarantee::Deterministic
437 );
438 assert_eq!(
439 s.capture_resume_position().await.unwrap(),
440 Some(json!("pos"))
441 );
442 }
443}