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 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}
135
136pub struct SamplingSource {
138 inner: Box<dyn Source>,
139 state: std::sync::Arc<SampleState>,
140}
141
142impl SamplingSource {
143 pub fn new(inner: Box<dyn Source>, state: std::sync::Arc<SampleState>) -> Self {
144 Self { inner, state }
145 }
146}
147
148#[async_trait]
149impl Source for SamplingSource {
150 async fn fetch_with_context(
151 &self,
152 ctx: &HashMap<String, Value>,
153 ) -> Result<Vec<Value>, FaucetError> {
154 self.inner.fetch_with_context(ctx).await
157 }
158 fn stream_pages<'a>(
162 &'a self,
163 ctx: &'a HashMap<String, Value>,
164 batch_size: usize,
165 ) -> Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>> {
166 let state = std::sync::Arc::clone(&self.state);
167 let inner = self.inner.stream_pages(ctx, batch_size);
168 Box::pin(faucet_core::async_stream::try_stream! {
169 let mut inner = inner;
170 while let Some(page) = inner.next().await {
171 let page = page?;
172 state.observe(&page.records);
173 yield page;
174 }
175 })
176 }
177 fn connector_name(&self) -> &'static str {
178 self.inner.connector_name()
179 }
180 fn dataset_uri(&self) -> String {
181 self.inner.dataset_uri()
182 }
183 fn state_key(&self) -> Option<String> {
184 self.inner.state_key()
185 }
186 async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
187 self.inner.apply_start_bookmark(bookmark).await
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use async_trait::async_trait;
195 use faucet_core::{FaucetError, Sink};
196 use serde_json::{Value, json};
197 use std::sync::Arc;
198
199 struct CollectSink(std::sync::Mutex<Vec<Value>>);
200 #[async_trait]
201 impl Sink for CollectSink {
202 async fn write_batch(&self, records: &[Value]) -> Result<usize, FaucetError> {
203 self.0.lock().unwrap().extend(records.iter().cloned());
204 Ok(records.len())
205 }
206 fn connector_name(&self) -> &'static str {
207 "collect"
208 }
209 }
210
211 #[tokio::test]
212 async fn sink_counts_and_samples_first_n() {
213 let shared = Arc::new(SampleState::new(2));
214 let inner: Box<dyn Sink> = Box::new(CollectSink(Default::default()));
215 let s = SamplingSink::new(inner, Arc::clone(&shared));
216 s.write_batch(&[json!({"id":1,"name":"a"})]).await.unwrap();
217 s.write_batch(&[json!({"id":2}), json!({"id":3})])
218 .await
219 .unwrap();
220 assert_eq!(shared.count(), 3);
221 let schema = shared.inferred_schema();
223 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
224 assert!(names.contains(&"id"));
225 assert!(names.contains(&"name"));
226 }
227
228 struct TwoRowSource;
229 #[async_trait]
230 impl faucet_core::Source for TwoRowSource {
231 async fn fetch_with_context(
232 &self,
233 _: &std::collections::HashMap<String, Value>,
234 ) -> Result<Vec<Value>, FaucetError> {
235 Ok(vec![json!({"id": 1}), json!({"id": 2})])
236 }
237 fn connector_name(&self) -> &'static str {
238 "tworow"
239 }
240 }
241
242 #[tokio::test]
243 async fn source_samples_streamed_pages_without_buffering_override() {
244 use faucet_core::Source as _;
245 use futures::StreamExt as _;
246 let shared = Arc::new(SampleState::new(10));
247 let s = SamplingSource::new(Box::new(TwoRowSource), Arc::clone(&shared));
248 let ctx = std::collections::HashMap::new();
249 let mut pages = s.stream_pages(&ctx, 1000);
250 while let Some(p) = pages.next().await {
251 let _ = p.unwrap();
252 }
253 assert_eq!(shared.count(), 2);
254 let schema = shared.inferred_schema();
255 let names: Vec<&str> = schema.fields.iter().map(|(n, _)| n.as_str()).collect();
256 assert!(names.contains(&"id"));
257 }
258}