use crate::capabilities::Capabilities;
use crate::check::{CheckContext, CheckReport, Probe};
use crate::error::FaucetError;
use crate::io::{CallContext, CallSlot};
use crate::observability::RoundtripRecorder;
use crate::pipeline::StreamPage;
use crate::traits::Source;
use async_trait::async_trait;
use futures::StreamExt;
use futures_core::Stream;
use serde_json::Value;
use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
pub type PageStream<'a> = Pin<Box<dyn Stream<Item = Result<StreamPage, FaucetError>> + Send + 'a>>;
#[async_trait]
pub trait SourceDriver: Send + Sync {
fn connector_name(&self) -> &'static str;
fn config_schema(&self) -> Value {
serde_json::json!({"type": "object", "properties": {}})
}
fn dataset_uri(&self) -> String {
format!("{}://unknown", self.connector_name())
}
fn call_context(&self) -> CallContext {
CallContext::new()
}
fn read<'a>(
&'a self,
calls: &'a CallSlot,
context: &'a HashMap<String, Value>,
batch_size: usize,
) -> PageStream<'a>;
fn state_key(&self) -> Option<String> {
None
}
async fn resume_from(&self, bookmark: Value) -> Result<(), FaucetError> {
let _ = bookmark;
Ok(())
}
fn deterministic_replay(&self) -> bool {
false
}
fn consumes_destructively(&self) -> bool {
false
}
fn set_cancel(&self, cancel: crate::CancellationToken) -> bool {
let _ = cancel;
false
}
async fn check(
&self,
calls: &CallSlot,
ctx: &CheckContext,
) -> Result<Option<CheckReport>, FaucetError> {
let _ = (calls, ctx);
Ok(None)
}
fn capabilities(&self, derived: Capabilities) -> Capabilities {
derived
}
async fn fetch(
&self,
calls: &CallSlot,
context: &HashMap<String, Value>,
) -> Option<Result<(Vec<Value>, Option<Value>), FaucetError>> {
let _ = (calls, context);
None
}
fn set_run_clock(&self, now: chrono::DateTime<chrono::Utc>) {
let _ = now;
}
fn set_call_context(&self, ctx: CallContext) {
let _ = ctx;
}
fn set_roundtrip_recorder(&self, recorder: Arc<RoundtripRecorder>) {
let _ = recorder;
}
fn native_output_formats(&self) -> &'static [crate::native::NativeFormat] {
&[]
}
fn read_native<'a>(
&'a self,
calls: &'a CallSlot,
context: &'a HashMap<String, Value>,
format: crate::native::NativeFormat,
batch_size: usize,
) -> NativeStream<'a> {
let _ = (calls, context, format, batch_size);
let name = self.connector_name();
let err: Result<crate::native::NativeBatch, FaucetError> = Err(FaucetError::Source(
format!("source '{name}' does not support native byte streaming (stream_native)"),
));
Box::pin(futures::stream::once(async move { err }))
}
#[cfg(feature = "arrow")]
fn supports_columnar(&self) -> bool {
false
}
#[cfg(feature = "arrow")]
fn read_columnar<'a>(
&'a self,
calls: &'a CallSlot,
context: &'a HashMap<String, Value>,
batch_size: usize,
) -> ColumnarStream<'a> {
let _ = (calls, context, batch_size);
let name = self.connector_name();
let err: Result<crate::columnar::ColumnarPage, FaucetError> = Err(FaucetError::Source(
format!("source '{name}' does not support columnar streaming (stream_batches)"),
));
Box::pin(futures::stream::once(async move { err }))
}
fn supports_discover(&self) -> bool {
false
}
async fn discover(
&self,
calls: &CallSlot,
) -> Result<Vec<crate::discover::DatasetDescriptor>, FaucetError> {
let _ = calls;
Err(FaucetError::Source(format!(
"source '{}' does not support dataset discovery",
self.connector_name()
)))
}
}
pub type NativeStream<'a> =
Pin<Box<dyn Stream<Item = Result<crate::native::NativeBatch, FaucetError>> + Send + 'a>>;
#[cfg(feature = "arrow")]
pub type ColumnarStream<'a> =
Pin<Box<dyn Stream<Item = Result<crate::columnar::ColumnarPage, FaucetError>> + Send + 'a>>;
pub fn pages_of(
records: Vec<Value>,
bookmark: Option<Value>,
batch_size: usize,
) -> impl Iterator<Item = StreamPage> + Send {
let chunk = if batch_size == 0 {
usize::MAX
} else {
batch_size
};
let total = records.len();
let empty = (total == 0 && bookmark.is_some()).then(|| StreamPage {
records: Vec::new(),
bookmark: bookmark.clone(),
});
let mut iter = records.into_iter();
let mut consumed = 0usize;
let pages = std::iter::from_fn(move || {
let batch: Vec<Value> = iter.by_ref().take(chunk).collect();
if batch.is_empty() {
return None;
}
consumed += batch.len();
Some(StreamPage {
bookmark: if consumed >= total {
bookmark.clone()
} else {
None
},
records: batch,
})
});
empty.into_iter().chain(pages)
}
pub struct DriverSource<D> {
driver: D,
calls: CallSlot,
}
impl<D: SourceDriver> DriverSource<D> {
pub fn new(driver: D) -> Self {
let calls = CallSlot::new(driver.call_context());
Self { driver, calls }
}
pub fn driver(&self) -> &D {
&self.driver
}
pub fn driver_mut(&mut self) -> &mut D {
&mut self.driver
}
pub fn calls(&self) -> &CallSlot {
&self.calls
}
pub fn into_driver(self) -> D {
self.driver
}
async fn drain(
&self,
context: &HashMap<String, Value>,
) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
if let Some(fetched) = self.driver.fetch(&self.calls, context).await {
return fetched;
}
let mut pages = self.driver.read(&self.calls, context, 0);
let mut rows = Vec::new();
let mut bookmark = None;
while let Some(page) = pages.next().await {
let page = page?;
rows.extend(page.records);
if page.bookmark.is_some() {
bookmark = page.bookmark;
}
}
Ok((rows, bookmark))
}
}
#[async_trait]
impl<D: SourceDriver> Source for DriverSource<D> {
async fn fetch_with_context(
&self,
context: &HashMap<String, Value>,
) -> Result<Vec<Value>, FaucetError> {
Ok(self.drain(context).await?.0)
}
async fn fetch_with_context_incremental(
&self,
context: &HashMap<String, Value>,
) -> Result<(Vec<Value>, Option<Value>), FaucetError> {
self.drain(context).await
}
fn stream_pages<'a>(
&'a self,
context: &'a HashMap<String, Value>,
batch_size: usize,
) -> PageStream<'a> {
self.driver.read(&self.calls, context, batch_size)
}
fn config_schema(&self) -> Value {
self.driver.config_schema()
}
fn state_key(&self) -> Option<String> {
self.driver.state_key()
}
async fn apply_start_bookmark(&self, bookmark: Value) -> Result<(), FaucetError> {
self.driver.resume_from(bookmark).await
}
fn supports_exactly_once(&self) -> bool {
self.driver.deterministic_replay()
}
fn consumes_destructively(&self) -> bool {
self.driver.consumes_destructively()
}
fn capabilities(&self) -> Capabilities {
self.driver.capabilities(Capabilities::of_source(self))
}
fn connector_name(&self) -> &'static str {
self.driver.connector_name()
}
fn set_roundtrip_recorder(&self, recorder: Arc<RoundtripRecorder>) {
self.calls.install_recorder(Arc::clone(&recorder));
self.driver.set_roundtrip_recorder(recorder);
}
fn set_call_context(&self, ctx: CallContext) {
self.calls.install(ctx.clone());
self.driver.set_call_context(ctx);
}
fn set_run_clock(&self, now: chrono::DateTime<chrono::Utc>) {
self.driver.set_run_clock(now);
}
fn native_output_formats(&self) -> &'static [crate::native::NativeFormat] {
self.driver.native_output_formats()
}
fn stream_native<'a>(
&'a self,
context: &'a HashMap<String, Value>,
format: crate::native::NativeFormat,
batch_size: usize,
) -> NativeStream<'a> {
self.driver
.read_native(&self.calls, context, format, batch_size)
}
#[cfg(feature = "arrow")]
fn supports_columnar(&self) -> bool {
self.driver.supports_columnar()
}
#[cfg(feature = "arrow")]
fn stream_batches<'a>(
&'a self,
context: &'a HashMap<String, Value>,
batch_size: usize,
) -> ColumnarStream<'a> {
self.driver.read_columnar(&self.calls, context, batch_size)
}
fn supports_discover(&self) -> bool {
self.driver.supports_discover()
}
async fn discover(&self) -> Result<Vec<crate::discover::DatasetDescriptor>, FaucetError> {
self.driver.discover(&self.calls).await
}
fn set_cancel(&self, cancel: crate::CancellationToken) -> bool {
self.driver.set_cancel(cancel)
}
fn dataset_uri(&self) -> String {
self.driver.dataset_uri()
}
async fn check(&self, ctx: &CheckContext) -> Result<CheckReport, FaucetError> {
if let Some(report) = self.driver.check(&self.calls, ctx).await? {
return Ok(report);
}
let empty = HashMap::new();
let start = std::time::Instant::now();
let mut pages = self.driver.read(&self.calls, &empty, 1);
let probe = match tokio::time::timeout(ctx.timeout, pages.next()).await {
Err(_) => Probe::fail("read", start.elapsed(), "timed out fetching first page"),
Ok(None) | Ok(Some(Ok(_))) => Probe::pass("read", start.elapsed()),
Ok(Some(Err(e))) => Probe::fail("read", start.elapsed(), e.to_string()),
};
Ok(CheckReport::single(probe))
}
}