1use crate::capabilities::Capabilities;
4use crate::check::{CheckContext, CheckReport, Probe};
5use crate::error::FaucetError;
6use crate::io::{CallContext, CallSlot};
7use crate::observability::RoundtripRecorder;
8use crate::pipeline::StreamPage;
9use crate::traits::Source;
10use async_trait::async_trait;
11use futures::StreamExt;
12use futures_core::Stream;
13use serde_json::Value;
14use std::collections::HashMap;
15use std::pin::Pin;
16use std::sync::Arc;
17
18pub type PageStream<'a> = Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>>;
20
21#[async_trait]
60pub trait SourceDriver: Send + Sync {
61 fn connector_name(&self) -> &'static str;
63
64 fn config_schema(&self) -> Value {
66 serde_json::json!({"type": "object", "properties": {}})
67 }
68
69 fn dataset_uri(&self) -> String {
71 format!("{}://unknown", self.connector_name())
72 }
73
74 fn call_context(&self) -> CallContext {
77 CallContext::new()
78 }
79
80 fn read<'a>(
83 &'a self,
84 calls: &'a CallSlot,
85 context: &'a HashMap<String, Value>,
86 batch_size: usize,
87 ) -> PageStream<'a>;
88
89 fn state_key(&self) -> Option<String> {
91 None
92 }
93
94 async fn resume_from(&self, bookmark: Value) -> Result<(), FaucetError> {
96 let _ = bookmark;
97 Ok(())
98 }
99
100 fn deterministic_replay(&self) -> bool {
102 false
103 }
104
105 fn consumes_destructively(&self) -> bool {
107 false
108 }
109
110 fn set_cancel(&self, cancel: crate::CancellationToken) -> bool {
113 let _ = cancel;
114 false
115 }
116
117 async fn check(
119 &self,
120 calls: &CallSlot,
121 ctx: &CheckContext,
122 ) -> Result<Option<CheckReport>, FaucetError> {
123 let _ = (calls, ctx);
124 Ok(None)
125 }
126
127 fn capabilities(&self, derived: Capabilities) -> Capabilities {
129 derived
130 }
131
132 async fn fetch(
136 &self,
137 calls: &CallSlot,
138 context: &HashMap<String, Value>,
139 ) -> Option<Result<(Vec<Value>, Option<Value>), FaucetError>> {
140 let _ = (calls, context);
141 None
142 }
143
144 fn set_run_clock(&self, now: chrono::DateTime<chrono::Utc>) {
146 let _ = now;
147 }
148
149 fn set_call_context(&self, ctx: CallContext) {
151 let _ = ctx;
152 }
153
154 fn set_roundtrip_recorder(&self, recorder: Arc<RoundtripRecorder>) {
157 let _ = recorder;
158 }
159
160 fn native_output_formats(&self) -> &'static [crate::native::NativeFormat] {
162 &[]
163 }
164
165 fn read_native<'a>(
167 &'a self,
168 calls: &'a CallSlot,
169 context: &'a HashMap<String, Value>,
170 format: crate::native::NativeFormat,
171 batch_size: usize,
172 ) -> NativeStream<'a> {
173 let _ = (calls, context, format, batch_size);
174 let name = self.connector_name();
175 let err: Result<crate::native::NativeBatch, FaucetError> = Err(FaucetError::Source(
176 format!("source '{name}' does not support native byte streaming (stream_native)"),
177 ));
178 Box::pin(futures::stream::once(async move { err }))
179 }
180
181 #[cfg(feature = "arrow")]
183 fn supports_columnar(&self) -> bool {
184 false
185 }
186
187 #[cfg(feature = "arrow")]
189 fn read_columnar<'a>(
190 &'a self,
191 calls: &'a CallSlot,
192 context: &'a HashMap<String, Value>,
193 batch_size: usize,
194 ) -> ColumnarStream<'a> {
195 let _ = (calls, context, batch_size);
196 let name = self.connector_name();
197 let err: Result<crate::columnar::ColumnarPage, FaucetError> = Err(FaucetError::Source(
198 format!("source '{name}' does not support columnar streaming (stream_batches)"),
199 ));
200 Box::pin(futures::stream::once(async move { err }))
201 }
202
203 fn supports_discover(&self) -> bool {
205 false
206 }
207
208 async fn declared_columns(
212 &self,
213 calls: &CallSlot,
214 context: &HashMap<String, Value>,
215 ) -> Result<Option<crate::declared::DeclaredColumns>, FaucetError> {
216 let _ = (calls, context);
217 Ok(None)
218 }
219
220 async fn discover(
221 &self,
222 calls: &CallSlot,
223 ) -> Result<Vec<crate::discover::DatasetDescriptor>, FaucetError> {
224 let _ = calls;
225 Err(FaucetError::Source(format!(
226 "source '{}' does not support dataset discovery",
227 self.connector_name()
228 )))
229 }
230}
231
232pub type NativeStream<'a> =
234 Pin<Box<dyn Stream<Item = Result<crate::native::NativeBatch, FaucetError>> + Send + 'a>>;
235
236#[cfg(feature = "arrow")]
238pub type ColumnarStream<'a> =
239 Pin<Box<dyn Stream<Item = Result<crate::columnar::ColumnarPage, FaucetError>> + Send + 'a>>;
240
241pub fn pages_of(
256 records: Vec<Value>,
257 bookmark: Option<Value>,
258 batch_size: usize,
259) -> impl Iterator<Item = StreamPage> + Send {
260 let chunk = if batch_size == 0 {
261 usize::MAX
262 } else {
263 batch_size
264 };
265 let total = records.len();
266 let empty = (total == 0 && bookmark.is_some()).then(|| StreamPage {
267 records: Vec::new(),
268 bookmark: bookmark.clone(),
269 });
270 let mut iter = records.into_iter();
271 let mut consumed = 0usize;
272 let pages = std::iter::from_fn(move || {
273 let batch: Vec<Value> = iter.by_ref().take(chunk).collect();
274 if batch.is_empty() {
275 return None;
276 }
277 consumed += batch.len();
278 Some(StreamPage {
279 bookmark: if consumed >= total {
280 bookmark.clone()
281 } else {
282 None
283 },
284 records: batch,
285 })
286 });
287 empty.into_iter().chain(pages)
288}
289
290pub struct DriverSource<D> {
293 driver: D,
294 calls: CallSlot,
295}
296
297impl<D: SourceDriver> DriverSource<D> {
298 pub fn new(driver: D) -> Self {
300 let calls = CallSlot::new(driver.call_context());
301 Self { driver, calls }
302 }
303
304 pub fn driver(&self) -> &D {
306 &self.driver
307 }
308
309 pub fn driver_mut(&mut self) -> &mut D {
311 &mut self.driver
312 }
313
314 pub fn calls(&self) -> &CallSlot {
316 &self.calls
317 }
318
319 pub fn into_driver(self) -> D {
321 self.driver
322 }
323
324 async fn drain(
325 &self,
326 context: &HashMap<String, Value>,
327 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
328 if let Some(fetched) = self.driver.fetch(&self.calls, context).await {
329 return fetched;
330 }
331 let mut pages = self.driver.read(&self.calls, context, 0);
332 let mut rows = Vec::new();
333 let mut bookmark = None;
334 while let Some(page) = pages.next().await {
335 let page = page?;
336 rows.extend(page.records);
337 if page.bookmark.is_some() {
338 bookmark = page.bookmark;
339 }
340 }
341 Ok((rows, bookmark))
342 }
343}
344
345#[async_trait]
346impl<D: SourceDriver> Source for DriverSource<D> {
347 async fn fetch_with_context(
348 &self,
349 context: &HashMap<String, Value>,
350 ) -> Result<Vec<Value>, FaucetError> {
351 Ok(self.drain(context).await?.0)
352 }
353
354 async fn fetch_with_context_incremental(
355 &self,
356 context: &HashMap<String, Value>,
357 ) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
358 self.drain(context).await
359 }
360
361 fn stream_pages<'a>(
362 &'a self,
363 context: &'a HashMap<String, Value>,
364 batch_size: usize,
365 ) -> PageStream<'a> {
366 self.driver.read(&self.calls, context, batch_size)
367 }
368
369 fn config_schema(&self) -> Value {
370 self.driver.config_schema()
371 }
372
373 fn state_key(&self) -> Option<String> {
374 self.driver.state_key()
375 }
376
377 async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
378 self.driver.resume_from(bookmark).await
379 }
380
381 fn supports_exactly_once(&self) -> bool {
382 self.driver.deterministic_replay()
383 }
384
385 fn consumes_destructively(&self) -> bool {
386 self.driver.consumes_destructively()
387 }
388
389 fn capabilities(&self) -> Capabilities {
390 self.driver.capabilities(Capabilities::of_source(self))
391 }
392
393 fn connector_name(&self) -> &'static str {
394 self.driver.connector_name()
395 }
396
397 fn set_roundtrip_recorder(&self, recorder: Arc<RoundtripRecorder>) {
398 self.calls.install_recorder(Arc::clone(&recorder));
399 self.driver.set_roundtrip_recorder(recorder);
400 }
401
402 fn set_call_context(&self, ctx: CallContext) {
403 self.calls.install(ctx.clone());
404 self.driver.set_call_context(ctx);
405 }
406
407 fn set_run_clock(&self, now: chrono::DateTime<chrono::Utc>) {
408 self.driver.set_run_clock(now);
409 }
410
411 fn native_output_formats(&self) -> &'static [crate::native::NativeFormat] {
412 self.driver.native_output_formats()
413 }
414
415 fn stream_native<'a>(
416 &'a self,
417 context: &'a HashMap<String, Value>,
418 format: crate::native::NativeFormat,
419 batch_size: usize,
420 ) -> NativeStream<'a> {
421 self.driver
422 .read_native(&self.calls, context, format, batch_size)
423 }
424
425 #[cfg(feature = "arrow")]
426 fn supports_columnar(&self) -> bool {
427 self.driver.supports_columnar()
428 }
429
430 #[cfg(feature = "arrow")]
431 fn stream_batches<'a>(
432 &'a self,
433 context: &'a HashMap<String, Value>,
434 batch_size: usize,
435 ) -> ColumnarStream<'a> {
436 self.driver.read_columnar(&self.calls, context, batch_size)
437 }
438
439 fn supports_discover(&self) -> bool {
440 self.driver.supports_discover()
441 }
442
443 async fn declared_columns(
444 &self,
445 context: &HashMap<String, Value>,
446 ) -> Result<Option<crate::declared::DeclaredColumns>, FaucetError> {
447 self.driver.declared_columns(&self.calls, context).await
448 }
449
450 async fn discover(&self) -> Result<Vec<crate::discover::DatasetDescriptor>, FaucetError> {
451 self.driver.discover(&self.calls).await
452 }
453
454 fn set_cancel(&self, cancel: crate::CancellationToken) -> bool {
455 self.driver.set_cancel(cancel)
456 }
457
458 fn dataset_uri(&self) -> String {
459 self.driver.dataset_uri()
460 }
461
462 async fn check(&self, ctx: &CheckContext) -> Result<CheckReport, FaucetError> {
463 if let Some(report) = self.driver.check(&self.calls, ctx).await? {
464 return Ok(report);
465 }
466 let empty = HashMap::new();
467 let start = std::time::Instant::now();
468 let mut pages = self.driver.read(&self.calls, &empty, 1);
469 let probe = match tokio::time::timeout(ctx.timeout, pages.next()).await {
470 Err(_) => Probe::fail("read", start.elapsed(), "timed out fetching first page"),
471 Ok(None) | Ok(Some(Ok(_))) => Probe::pass("read", start.elapsed()),
472 Ok(Some(Err(e))) => Probe::fail("read", start.elapsed(), e.to_string()),
473 };
474 Ok(CheckReport::single(probe))
475 }
476}