faucet_lineage/
sampling.rs1use 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 inferred_schema(&self) -> InferredSchema {
50 let sample = self.sample.lock().unwrap();
51 if sample.is_empty() {
52 return InferredSchema::default();
53 }
54 let mut order: Vec<String> = Vec::new();
56 let mut types: HashMap<String, String> = HashMap::new();
57 for rec in sample.iter() {
58 if let Value::Object(map) = rec {
59 for (k, v) in map {
60 if !types.contains_key(k) {
61 order.push(k.clone());
62 }
63 types
64 .entry(k.clone())
65 .or_insert_with(|| ol_type_of(v).to_string());
66 }
67 }
68 }
69 InferredSchema {
70 fields: order
71 .into_iter()
72 .map(|k| {
73 let t = types.remove(&k).unwrap_or_else(|| "string".into());
74 (k, t)
75 })
76 .collect(),
77 }
78 }
79}
80
81fn ol_type_of(v: &Value) -> &'static str {
82 match v {
83 Value::Null => "null",
84 Value::Bool(_) => "boolean",
85 Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
86 Value::Number(_) => "number",
87 Value::String(_) => "string",
88 Value::Array(_) => "array",
89 Value::Object(_) => "object",
90 }
91}
92
93pub struct SamplingSink {
95 inner: Box<dyn Sink>,
96 state: std::sync::Arc<SampleState>,
97}
98
99impl SamplingSink {
100 pub fn new(inner: Box<dyn Sink>, state: std::sync::Arc<SampleState>) -> Self {
101 Self { inner, state }
102 }
103}
104
105#[async_trait]
106impl Sink for SamplingSink {
107 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
108 let n = self.inner.write_batch(records).await?;
109 self.state.observe(records);
110 Ok(n)
111 }
112 async fn write_batch_partial(&self, records: &[Value]) -> Result<Vec<RowOutcome>, FaucetError> {
113 let outcomes = self.inner.write_batch_partial(records).await?;
114 self.state.observe(records);
115 Ok(outcomes)
116 }
117 async fn flush(&self) -> Result<(), FaucetError> {
118 self.inner.flush().await
119 }
120 fn connector_name(&self) -> &'static str {
121 self.inner.connector_name()
122 }
123 fn dataset_uri(&self) -> String {
124 self.inner.dataset_uri()
125 }
126}
127
128pub struct SamplingSource {
130 inner: Box<dyn Source>,
131 state: std::sync::Arc<SampleState>,
132}
133
134impl SamplingSource {
135 pub fn new(inner: Box<dyn Source>, state: std::sync::Arc<SampleState>) -> Self {
136 Self { inner, state }
137 }
138}
139
140#[async_trait]
141impl Source for SamplingSource {
142 async fn fetch_with_context(
143 &self,
144 ctx: &HashMap<String, Value>,
145 ) -> Result<Vec<Value>, FaucetError> {
146 self.inner.fetch_with_context(ctx).await
149 }
150 fn stream_pages<'a>(
154 &'a self,
155 ctx: &'a HashMap<String, Value>,
156 batch_size: usize,
157 ) -> Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>> {
158 let state = std::sync::Arc::clone(&self.state);
159 let inner = self.inner.stream_pages(ctx, batch_size);
160 Box::pin(faucet_core::async_stream::try_stream! {
161 let mut inner = inner;
162 while let Some(page) = inner.next().await {
163 let page = page?;
164 state.observe(&page.records);
165 yield page;
166 }
167 })
168 }
169 fn connector_name(&self) -> &'static str {
170 self.inner.connector_name()
171 }
172 fn dataset_uri(&self) -> String {
173 self.inner.dataset_uri()
174 }
175 fn state_key(&self) -> Option<String> {
176 self.inner.state_key()
177 }
178 async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
179 self.inner.apply_start_bookmark(bookmark).await
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use async_trait::async_trait;
187 use faucet_core::{FaucetError, Sink};
188 use serde_json::{Value, json};
189 use std::sync::Arc;
190
191 struct CollectSink(std::sync::Mutex<Vec<Value>>);
192 #[async_trait]
193 impl Sink for CollectSink {
194 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
195 self.0.lock().unwrap().extend(records.iter().cloned());
196 Ok(records.len())
197 }
198 fn connector_name(&self) -> &'static str {
199 "collect"
200 }
201 }
202
203 #[tokio::test]
204 async fn sink_counts_and_samples_first_n() {
205 let shared = Arc::new(SampleState::new(2));
206 let inner: Box<dyn Sink> = Box::new(CollectSink(Default::default()));
207 let s = SamplingSink::new(inner, Arc::clone(&shared));
208 s.write_batch(&[json!({"id":1,"name":"a"})]).await.unwrap();
209 s.write_batch(&[json!({"id":2}), json!({"id":3})])
210 .await
211 .unwrap();
212 assert_eq!(shared.count(), 3);
213 let schema = shared.inferred_schema();
215 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
216 assert!(names.contains(&"id"));
217 assert!(names.contains(&"name"));
218 }
219
220 struct TwoRowSource;
221 #[async_trait]
222 impl faucet_core::Source for TwoRowSource {
223 async fn fetch_with_context(
224 &self,
225 _: &std::collections::HashMap<String, Value>,
226 ) -> Result<Vec<Value>, FaucetError> {
227 Ok(vec![json!({"id": 1}), json!({"id": 2})])
228 }
229 fn connector_name(&self) -> &'static str {
230 "tworow"
231 }
232 }
233
234 #[tokio::test]
235 async fn source_samples_streamed_pages_without_buffering_override() {
236 use faucet_core::Source as _;
237 use futures::StreamExt as _;
238 let shared = Arc::new(SampleState::new(10));
239 let s = SamplingSource::new(Box::new(TwoRowSource), Arc::clone(&shared));
240 let ctx = std::collections::HashMap::new();
241 let mut pages = s.stream_pages(&ctx, 1000);
242 while let Some(p) = pages.next().await {
243 let _ = p.unwrap();
244 }
245 assert_eq!(shared.count(), 2);
246 let schema = shared.inferred_schema();
247 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
248 assert!(names.contains(&"id"));
249 }
250}